ntropi 1.0.0 → 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,1124 +0,0 @@
1
- import { useEffect, useRef, useState } from 'react'
2
- import { createPortal } from 'react-dom'
3
- import { Edit2, Plus, Type, Hash, ToggleLeft, Calendar, FileDigit, GripVertical, Trash2, Box, List, Copy, Check, ChevronUp, ChevronDown, Trash, HelpCircle, X } from 'lucide-react'
4
- // eslint-disable-next-line no-unused-vars
5
- import { motion, AnimatePresence } from 'framer-motion'
6
-
7
- function JsonTreeNode({
8
- label,
9
- value,
10
- depth = 0,
11
- path = [],
12
- onUpdateNode,
13
- onDuplicateNode,
14
- onDeleteNode,
15
- onReorderNode,
16
- isParentEditing = false,
17
- }) {
18
- const isArray = Array.isArray(value)
19
- const isObject = value !== null && typeof value === 'object' && !isArray
20
- const [isEditingNode, setIsEditingNode] = useState(false)
21
- const [isExpanded, setIsExpanded] = useState(depth < 1)
22
- const [repeatCount, setRepeatCount] = useState('1')
23
- const [isEditingRepeatCount, setIsEditingRepeatCount] = useState(false)
24
- const [newArrayItemValue, setNewArrayItemValue] = useState('')
25
- const [newObjectKey, setNewObjectKey] = useState('')
26
- const [newObjectValue, setNewObjectValue] = useState('')
27
- const primitiveEditorRef = useRef(null)
28
- const repeatCountInputRef = useRef(null)
29
-
30
- // Reset duplication controls whenever node value changes using render phase derivative state.
31
- const [prevValue, setPrevValue] = useState(value)
32
- if (value !== prevValue) {
33
- setPrevValue(value)
34
- setRepeatCount('1')
35
- setIsEditingRepeatCount(false)
36
- }
37
-
38
- useEffect(() => {
39
- if (isEditingRepeatCount && repeatCountInputRef.current) {
40
- repeatCountInputRef.current.focus()
41
- repeatCountInputRef.current.select()
42
- }
43
- }, [isEditingRepeatCount])
44
-
45
- const primitiveValue = typeof value === 'string' ? `"${value}"` : String(value)
46
-
47
- useEffect(() => {
48
- if (isArray || isObject || !primitiveEditorRef.current) {
49
- return
50
- }
51
-
52
- if (document.activeElement !== primitiveEditorRef.current) {
53
- primitiveEditorRef.current.textContent = primitiveValue
54
- }
55
- }, [primitiveValue, isArray, isObject])
56
-
57
- if (!isArray && !isObject) {
58
- const handlePrimitiveBlur = (event) => {
59
- if (!isParentEditing) {
60
- return
61
- }
62
-
63
- const rawValue = event.currentTarget.textContent ?? ''
64
- const trimmedValue = rawValue.trim()
65
-
66
- if (!trimmedValue) {
67
- onUpdateNode(path, '')
68
- return
69
- }
70
-
71
- try {
72
- onUpdateNode(path, JSON.parse(trimmedValue))
73
- } catch {
74
- onUpdateNode(path, rawValue)
75
- }
76
- }
77
-
78
- const handlePrimitiveKeyDown = (event) => {
79
- if (event.key === 'Enter') {
80
- event.preventDefault()
81
- event.currentTarget.blur()
82
- return
83
- }
84
-
85
- if (event.key === 'Escape') {
86
- event.preventDefault()
87
- event.currentTarget.textContent = primitiveValue
88
- event.currentTarget.blur()
89
- }
90
- }
91
-
92
- return (
93
- <div className={`flex items-center ${depth > 0 ? 'ml-1' : ''}`}>
94
- <span className="text-[13px] font-mono text-cyan-400">
95
- {label !== null ? `"${label}"` : ''}
96
- </span>
97
- <span className="text-[13px] font-mono text-neutral-500 mx-1">
98
- {label !== null ? ':' : ''}
99
- </span>
100
- {isParentEditing ? (
101
- <span
102
- ref={primitiveEditorRef}
103
- contentEditable
104
- suppressContentEditableWarning
105
- onBlur={handlePrimitiveBlur}
106
- onKeyDown={handlePrimitiveKeyDown}
107
- className="text-[13px] font-mono text-emerald-400 outline-none hover:bg-white/5 px-1 rounded transition-colors"
108
- >
109
- {primitiveValue}
110
- </span>
111
- ) : (
112
- <span className="text-[13px] font-mono text-emerald-400">{primitiveValue}</span>
113
- )}
114
- </div>
115
- )
116
- }
117
-
118
- const entries = isArray ? value.map((item, index) => [index, item]) : Object.entries(value)
119
- const closedTypeLabel = isArray ? `[${value.length}]` : `{${entries.length}}`
120
-
121
- const handleStartNodeEdit = (event) => {
122
- event.preventDefault()
123
- event.stopPropagation()
124
- setIsEditingNode((previous) => !previous)
125
- }
126
-
127
- const handleCancelNodeEdit = () => {
128
- setIsEditingNode(false)
129
- }
130
-
131
- const handleDetailsToggle = (event) => {
132
- const nextOpen = event.currentTarget.open
133
- setIsExpanded(nextOpen)
134
-
135
- if (!nextOpen) {
136
- handleCancelNodeEdit()
137
- setIsEditingRepeatCount(false)
138
- }
139
- }
140
-
141
- const handleApplyRepeatCount = () => {
142
- const parsedCount = Number.parseInt(repeatCount, 10)
143
- if (!Number.isInteger(parsedCount) || parsedCount < 1) {
144
- setRepeatCount('1')
145
- setIsEditingRepeatCount(false)
146
- return
147
- }
148
-
149
- if (parsedCount === 1) {
150
- setIsEditingRepeatCount(false)
151
- return
152
- }
153
-
154
- onDuplicateNode(path, parsedCount)
155
- setRepeatCount('1')
156
- setIsEditingRepeatCount(false)
157
- }
158
-
159
- const handleRepeatCountKeyDown = (event) => {
160
- if (event.key === 'Enter') {
161
- event.preventDefault()
162
- handleApplyRepeatCount()
163
- }
164
-
165
- if (event.key === 'Escape') {
166
- event.preventDefault()
167
- setRepeatCount('1')
168
- setIsEditingRepeatCount(false)
169
- }
170
- }
171
-
172
- const handleStartRepeatCountEdit = (event) => {
173
- event.preventDefault()
174
- event.stopPropagation()
175
- setIsEditingRepeatCount(true)
176
- }
177
-
178
- const handleDeleteNode = (event) => {
179
- event.preventDefault()
180
- event.stopPropagation()
181
- onDeleteNode(path)
182
- }
183
-
184
- const handleReorderEntry = (event, entryKey, direction) => {
185
- event.preventDefault()
186
- event.stopPropagation()
187
- onReorderNode([...path, entryKey], direction)
188
- }
189
-
190
- const handleDeleteEntry = (event, entryKey) => {
191
- event.preventDefault()
192
- event.stopPropagation()
193
- onDeleteNode([...path, entryKey])
194
- }
195
-
196
- const parseInlineValue = (rawValue) => {
197
- const trimmedValue = rawValue.trim()
198
- if (!trimmedValue) {
199
- return ''
200
- }
201
-
202
- try {
203
- return JSON.parse(trimmedValue)
204
- } catch {
205
- return rawValue
206
- }
207
- }
208
-
209
- const handleAddArrayItem = (event) => {
210
- event.preventDefault()
211
- event.stopPropagation()
212
-
213
- onUpdateNode(path, [...value, parseInlineValue(newArrayItemValue)])
214
- setNewArrayItemValue('')
215
- }
216
-
217
- const handleAddObjectEntry = (event) => {
218
- event.preventDefault()
219
- event.stopPropagation()
220
-
221
- const key = newObjectKey.trim()
222
- if (!key) {
223
- return
224
- }
225
-
226
- onUpdateNode(path, {
227
- ...value,
228
- [key]: parseInlineValue(newObjectValue),
229
- })
230
-
231
- setNewObjectKey('')
232
- setNewObjectValue('')
233
- }
234
-
235
- const handleAddInputKeyDown = (event, type) => {
236
- if (event.key !== 'Enter') {
237
- return
238
- }
239
-
240
- if (type === 'array') {
241
- handleAddArrayItem(event)
242
- return
243
- }
244
-
245
- handleAddObjectEntry(event)
246
- }
247
-
248
- const handleQuickAddContainer = (event, containerType) => {
249
- event.preventDefault()
250
- event.stopPropagation()
251
-
252
- const nextContainer = containerType === 'array' ? [] : {}
253
-
254
- if (isArray) {
255
- onUpdateNode(path, [...value, nextContainer])
256
- return
257
- }
258
-
259
- if (isObject) {
260
- const baseKey = containerType === 'array' ? 'newArray' : 'newObject'
261
- let nextKey = baseKey
262
- let index = 1
263
-
264
- while (Object.prototype.hasOwnProperty.call(value, nextKey)) {
265
- nextKey = `${baseKey}_${index}`
266
- index += 1
267
- }
268
-
269
- onUpdateNode(path, {
270
- ...value,
271
- [nextKey]: nextContainer,
272
- })
273
- }
274
- }
275
-
276
- const getPresetValue = (presetType) => {
277
- switch (presetType) {
278
- case 'name':
279
- return '$name'
280
- case 'string':
281
- return '$string(8)'
282
- case 'int':
283
- return '$int(1-10)'
284
- case 'float':
285
- return '$float(1.0-5.0)'
286
- case 'bool':
287
- return '$bool'
288
- case 'uuid':
289
- return '$uuid'
290
- case 'date':
291
- return '$date(DD-MM-YYYY)'
292
- default:
293
- return ''
294
- }
295
- }
296
-
297
- const getPresetObjectKey = (existingObject, presetType) => {
298
- const baseKey = presetType
299
- let nextKey = baseKey
300
- let index = 1
301
-
302
- while (Object.prototype.hasOwnProperty.call(existingObject, nextKey)) {
303
- nextKey = `${baseKey}_${index}`
304
- index += 1
305
- }
306
-
307
- return nextKey
308
- }
309
-
310
- const handleQuickAddPreset = (event, presetType) => {
311
- event.preventDefault()
312
- event.stopPropagation()
313
-
314
- const presetValue = getPresetValue(presetType)
315
-
316
- if (isArray) {
317
- onUpdateNode(path, [...value, presetValue])
318
- return
319
- }
320
-
321
- if (isObject) {
322
- const nextKey = getPresetObjectKey(value, presetType)
323
- onUpdateNode(path, {
324
- ...value,
325
- [nextKey]: presetValue,
326
- })
327
- }
328
- }
329
-
330
- return (
331
- <details
332
- open={isExpanded}
333
- onToggle={handleDetailsToggle}
334
- className={depth > 0 ? 'ml-1' : ''}
335
- >
336
- <summary className="cursor-pointer select-none text-[13px] font-mono text-neutral-300 marker:text-neutral-500 hover:text-white transition-colors">
337
- {label !== null ? <span className="text-cyan-400">"{label}"</span> : ''}
338
- {label !== null ? <span className="text-neutral-500 mx-1">:</span> : ''}
339
- {isArray ? (
340
- <span className="text-neutral-400">{isExpanded ? '[' : closedTypeLabel}</span>
341
- ) : (
342
- <span className="text-neutral-400">{isExpanded ? '{' : closedTypeLabel}</span>
343
- )}
344
- {isExpanded && (
345
- <div className="inline-flex items-center ml-3 opacity-0 group-open:opacity-100 group-hover:opacity-100 transition-opacity hover:opacity-100" onClick={(e) => e.preventDefault()}>
346
- <div className="flex items-center gap-0.5 rounded-lg border border-white/5 bg-[#0A0A0A] px-1 py-0.5 shadow-md">
347
- <button
348
- type="button"
349
- aria-label="Edit node"
350
- title={isEditingNode ? 'Stop editing' : 'Edit'}
351
- onMouseDown={(event) => {
352
- event.preventDefault()
353
- event.stopPropagation()
354
- }}
355
- onClick={handleStartNodeEdit}
356
- className={`flex h-6 w-6 items-center justify-center rounded-md transition-colors ${isEditingNode ? 'bg-indigo-500/20 text-indigo-400' : 'text-neutral-500 hover:bg-white/10 hover:text-neutral-200'}`}
357
- >
358
- <Edit2 size={13} />
359
- </button>
360
- <div className="h-3 w-px bg-white/10 mx-0.5"></div>
361
- <button
362
- type="button"
363
- aria-label="Add array"
364
- title="Add Array []"
365
- onMouseDown={(event) => {
366
- event.preventDefault()
367
- event.stopPropagation()
368
- }}
369
- onClick={(event) => handleQuickAddContainer(event, 'array')}
370
- className="flex h-6 w-6 items-center justify-center rounded-md text-emerald-500/70 transition-colors hover:bg-emerald-500/10 hover:text-emerald-400"
371
- >
372
- <List size={14} />
373
- </button>
374
- <button
375
- type="button"
376
- aria-label="Add object"
377
- title="Add Object {}"
378
- onMouseDown={(event) => {
379
- event.preventDefault()
380
- event.stopPropagation()
381
- }}
382
- onClick={(event) => handleQuickAddContainer(event, 'object')}
383
- className="flex h-6 w-6 items-center justify-center rounded-md text-blue-500/70 transition-colors hover:bg-blue-500/10 hover:text-blue-400"
384
- >
385
- <Box size={13} />
386
- </button>
387
-
388
- <div className="h-3 w-px bg-white/10 mx-0.5"></div>
389
-
390
- <button
391
- type="button"
392
- title="Add Name"
393
- onMouseDown={(e) => { e.preventDefault(); e.stopPropagation(); }}
394
- onClick={(e) => handleQuickAddPreset(e, 'name')}
395
- className="flex h-6 w-6 items-center justify-center rounded-md text-neutral-500 transition-colors hover:bg-white/10 hover:text-amber-400"
396
- >
397
- <Type size={13} />
398
- </button>
399
- <button
400
- type="button"
401
- title="Add String"
402
- onMouseDown={(e) => { e.preventDefault(); e.stopPropagation(); }}
403
- onClick={(e) => handleQuickAddPreset(e, 'string')}
404
- className="flex h-6 w-6 items-center justify-center rounded-md text-neutral-500 transition-colors hover:bg-white/10 hover:text-amber-400"
405
- >
406
- <Type size={13} />
407
- </button>
408
- <button
409
- type="button"
410
- title="Add Integer"
411
- onMouseDown={(e) => { e.preventDefault(); e.stopPropagation(); }}
412
- onClick={(e) => handleQuickAddPreset(e, 'int')}
413
- className="flex h-6 w-6 items-center justify-center rounded-md text-neutral-500 transition-colors hover:bg-white/10 hover:text-purple-400"
414
- >
415
- <Hash size={13} />
416
- </button>
417
- <button
418
- type="button"
419
- title="Add Float"
420
- onMouseDown={(e) => { e.preventDefault(); e.stopPropagation(); }}
421
- onClick={(e) => handleQuickAddPreset(e, 'float')}
422
- className="flex h-6 w-6 items-center justify-center rounded-md text-neutral-500 transition-colors hover:bg-white/10 hover:text-fuchsia-400"
423
- >
424
- <FileDigit size={13} />
425
- </button>
426
- <button
427
- type="button"
428
- title="Add Boolean"
429
- onMouseDown={(e) => { e.preventDefault(); e.stopPropagation(); }}
430
- onClick={(e) => handleQuickAddPreset(e, 'bool')}
431
- className="flex h-6 w-6 items-center justify-center rounded-md text-neutral-500 transition-colors hover:bg-white/10 hover:text-rose-400"
432
- >
433
- <ToggleLeft size={13} />
434
- </button>
435
- <button
436
- type="button"
437
- title="Add UUID"
438
- onMouseDown={(e) => { e.preventDefault(); e.stopPropagation(); }}
439
- onClick={(e) => handleQuickAddPreset(e, 'uuid')}
440
- className="flex h-6 w-6 items-center justify-center rounded-md text-neutral-500 transition-colors hover:bg-white/10 hover:text-indigo-400"
441
- >
442
- <FileDigit size={13} />
443
- </button>
444
- <button
445
- type="button"
446
- title="Add Date"
447
- onMouseDown={(e) => { e.preventDefault(); e.stopPropagation(); }}
448
- onClick={(e) => handleQuickAddPreset(e, 'date')}
449
- className="flex h-6 w-6 items-center justify-center rounded-md text-neutral-500 transition-colors hover:bg-white/10 hover:text-teal-400"
450
- >
451
- <Calendar size={13} />
452
- </button>
453
- </div>
454
- </div>
455
- )}
456
- </summary>
457
-
458
- <div className="mt-1.5 text-[13px] text-neutral-300 font-mono">
459
- {(isEditingNode || isParentEditing) && (
460
- <div
461
- className="mb-3 ml-3 flex max-w-105 flex-wrap items-center gap-2 rounded-md border border-white/5 bg-white/5 px-2.5 py-2 shadow-sm"
462
- onMouseDown={(event) => event.stopPropagation()}
463
- >
464
- {isArray ? (
465
- <>
466
- <input
467
- type="text"
468
- value={newArrayItemValue}
469
- onChange={(event) => setNewArrayItemValue(event.target.value)}
470
- onKeyDown={(event) => handleAddInputKeyDown(event, 'array')}
471
- placeholder="add value"
472
- className="flex-1 bg-transparent text-[13px] text-neutral-300 placeholder:text-neutral-600 focus:outline-none min-w-30"
473
- />
474
- <button
475
- type="button"
476
- onClick={handleAddArrayItem}
477
- className="inline-flex h-6 w-6 items-center justify-center rounded border border-white/10 bg-white/5 text-neutral-400 transition-colors hover:border-indigo-500/50 hover:bg-indigo-500/10 hover:text-indigo-400"
478
- >
479
- <Plus size={14} />
480
- </button>
481
- </>
482
- ) : (
483
- <>
484
- <input
485
- type="text"
486
- value={newObjectKey}
487
- onChange={(event) => setNewObjectKey(event.target.value)}
488
- onKeyDown={(event) => handleAddInputKeyDown(event, 'object')}
489
- placeholder="key"
490
- className="w-24 bg-transparent text-[13px] text-neutral-300 placeholder:text-neutral-600 focus:outline-none"
491
- />
492
- <span className="text-neutral-600">:</span>
493
- <input
494
- type="text"
495
- value={newObjectValue}
496
- onChange={(event) => setNewObjectValue(event.target.value)}
497
- onKeyDown={(event) => handleAddInputKeyDown(event, 'object')}
498
- placeholder="value"
499
- className="flex-1 bg-transparent text-[13px] text-neutral-300 placeholder:text-neutral-600 focus:outline-none min-w-20"
500
- />
501
- <button
502
- type="button"
503
- onClick={handleAddObjectEntry}
504
- className="inline-flex h-6 w-6 items-center justify-center rounded border border-white/10 bg-white/5 text-neutral-400 transition-colors hover:border-indigo-500/50 hover:bg-indigo-500/10 hover:text-indigo-400"
505
- >
506
- <Plus size={14} />
507
- </button>
508
- </>
509
- )}
510
- </div>
511
- )}
512
-
513
- {entries.length === 0 ? (
514
- <div className="ml-3 text-[13px] text-neutral-600 font-mono italic">(empty)</div>
515
- ) : (
516
- <div className="ml-3.5 border-l border-white/10 pl-3">
517
- {entries.map(([entryKey, entryValue], entryIndex) => (
518
- <div key={`${depth}-${String(entryKey)}`} className="group flex items-start gap-1.5 py-0.5">
519
- {isExpanded && (isEditingNode || isParentEditing) && (
520
- <div className="mt-1 flex flex-row gap-0.5 opacity-0 transition-opacity group-hover:opacity-100 items-start">
521
- <button
522
- type="button"
523
- aria-label="Move up"
524
- title="Move up"
525
- disabled={entryIndex === 0}
526
- onMouseDown={(event) => {
527
- event.preventDefault()
528
- event.stopPropagation()
529
- }}
530
- onClick={(event) => handleReorderEntry(event, entryKey, 'up')}
531
- className="h-5 w-5 rounded border border-white/5 bg-transparent text-neutral-500 transition-colors disabled:opacity-30 disabled:hover:border-white/5 disabled:hover:text-neutral-500 hover:border-white/20 hover:bg-white/5 hover:text-white flex items-center justify-center"
532
- >
533
- <ChevronUp size={12} />
534
- </button>
535
- <button
536
- type="button"
537
- aria-label="Move down"
538
- title="Move down"
539
- disabled={entryIndex === entries.length - 1}
540
- onMouseDown={(event) => {
541
- event.preventDefault()
542
- event.stopPropagation()
543
- }}
544
- onClick={(event) => handleReorderEntry(event, entryKey, 'down')}
545
- className="h-5 w-5 rounded border border-white/5 bg-transparent text-neutral-500 transition-colors disabled:opacity-30 disabled:hover:border-white/5 disabled:hover:text-neutral-500 hover:border-white/20 hover:bg-white/5 hover:text-white flex items-center justify-center"
546
- >
547
- <ChevronDown size={12} />
548
- </button>
549
- <button
550
- type="button"
551
- aria-label="Delete item"
552
- title="Delete item"
553
- onMouseDown={(event) => {
554
- event.preventDefault()
555
- event.stopPropagation()
556
- }}
557
- onClick={(event) => handleDeleteEntry(event, entryKey)}
558
- className="h-5 w-5 rounded border border-red-500/20 bg-transparent text-red-500/70 transition-colors hover:border-red-500/40 hover:bg-red-500/10 hover:text-red-400 flex items-center justify-center ml-0.5"
559
- >
560
- <Trash2 size={11} />
561
- </button>
562
- </div>
563
- )}
564
- <div className="flex-1 min-w-0">
565
- <JsonTreeNode
566
- label={isArray ? null : String(entryKey)}
567
- value={entryValue}
568
- depth={depth + 1}
569
- path={[...path, entryKey]}
570
- onUpdateNode={onUpdateNode}
571
- onDuplicateNode={onDuplicateNode}
572
- onDeleteNode={onDeleteNode}
573
- onReorderNode={onReorderNode}
574
- isParentEditing={isParentEditing || isEditingNode}
575
- />
576
- </div>
577
- </div>
578
- ))}
579
- </div>
580
- )}
581
-
582
- <div className="ml-1 mt-0.5 flex items-center text-neutral-500 font-mono">
583
- {isArray ? ']' : '}'}
584
- {path.length > 0 && (
585
- <div className="ml-3 flex items-center gap-2 opacity-70 transition-opacity hover:opacity-100">
586
- <span className="text-[11px] text-neutral-500 font-sans tracking-wide">Multiply Dataset:</span>
587
- <div className="flex items-center rounded-md border border-white/10 bg-[#0A0A0A] overflow-hidden focus-within:border-indigo-500/50 focus-within:ring-1 focus-within:ring-indigo-500/50 transition-all">
588
- <div className="flex items-center justify-center bg-white/5 px-2 py-1 border-r border-white/5 select-none" title="Repeat count">
589
- <span className="text-[12px] leading-none text-neutral-500">×</span>
590
- </div>
591
- <input
592
- ref={repeatCountInputRef}
593
- type="text"
594
- value={repeatCount}
595
- onChange={(event) =>
596
- setRepeatCount(event.target.value.replace(/[^0-9]/g, ''))
597
- }
598
- onBlur={handleApplyRepeatCount}
599
- onKeyDown={handleRepeatCountKeyDown}
600
- onMouseDown={(event) => {
601
- event.stopPropagation()
602
- }}
603
- onClick={(event) => event.stopPropagation()}
604
- placeholder="1"
605
- className="w-10 bg-transparent px-2 py-1 text-[12px] font-mono leading-none text-neutral-300 focus:outline-none"
606
- title="Number of times to duplicate this node"
607
- />
608
- </div>
609
- <button
610
- type="button"
611
- aria-label="Delete node"
612
- title="Delete"
613
- onMouseDown={(event) => {
614
- event.preventDefault()
615
- event.stopPropagation()
616
- }}
617
- onClick={handleDeleteNode}
618
- className="flex h-6 w-6 items-center justify-center rounded-md border border-red-500/20 bg-transparent text-red-500/70 transition-colors hover:border-red-500/40 hover:bg-red-500/10 hover:text-red-400 cursor-pointer"
619
- >
620
- <Trash2 size={13} />
621
- </button>
622
- </div>
623
- )}
624
- </div>
625
- </div>
626
- </details>
627
- )
628
- }
629
-
630
- export default function DatasetEditor({ isOpen, endpoint, endpointIndex, onClose, onSaveField }) {
631
- const [editingField, setEditingField] = useState(null)
632
- const [editorValue, setEditorValue] = useState('')
633
- const [showHelp, setShowHelp] = useState(false)
634
- const editorRef = useRef(null)
635
-
636
- useEffect(() => {
637
- if (!editorRef.current || !editingField) {
638
- return
639
- }
640
-
641
- const editorElement = editorRef.current
642
- editorElement.style.height = 'auto'
643
-
644
- const maxHeight = window.innerHeight * 0.5
645
- const nextHeight = Math.min(editorElement.scrollHeight, maxHeight)
646
- editorElement.style.height = `${nextHeight}px`
647
- editorElement.style.overflowY = 'hidden'
648
- }, [editorValue, editingField])
649
-
650
- const formatEndpointValue = (value) => {
651
- if (typeof value === 'string') {
652
- return value
653
- }
654
-
655
- return JSON.stringify(value, null, 2)
656
- }
657
-
658
- const parseEditorValue = (value) => {
659
- const trimmedValue = value.trim()
660
-
661
- if (!trimmedValue) {
662
- return ''
663
- }
664
-
665
- try {
666
- return JSON.parse(trimmedValue)
667
- } catch {
668
- return value
669
- }
670
- }
671
-
672
- const handleStartFieldEdit = (field) => {
673
- if (!endpoint) {
674
- return
675
- }
676
-
677
- setEditingField(field)
678
- setEditorValue(formatEndpointValue(endpoint[field]))
679
- }
680
-
681
- const getPreviewClassName = () => {
682
- return 'max-h-[50vh] cursor-text overflow-y-auto whitespace-pre-wrap wrap-break-word rounded-xl border border-white/5 bg-[#0c0c0c] p-4 text-[13px] font-mono text-neutral-300'
683
- }
684
-
685
- const getEditorClassName = () => {
686
- return 'max-h-[50vh] w-full resize-none overflow-hidden rounded-xl border border-white/10 bg-[#0c0c0c] p-4 text-[13px] font-mono text-neutral-300 focus:border-indigo-500/50 focus:ring-1 focus:ring-indigo-500/50 focus:outline-none transition-all'
687
- }
688
-
689
- const handleCancelFieldEdit = () => {
690
- setEditingField(null)
691
- setEditorValue('')
692
- }
693
-
694
- const handleCloseDrawer = () => {
695
- handleCancelFieldEdit()
696
- onClose()
697
- }
698
-
699
- const handleSaveFieldEdit = () => {
700
- if (!editingField || endpointIndex === null) {
701
- return
702
- }
703
-
704
- onSaveField(endpointIndex, editingField, parseEditorValue(editorValue))
705
- handleCancelFieldEdit()
706
- }
707
-
708
- const handleEditorKeyDown = (event) => {
709
- if ((event.ctrlKey || event.metaKey) && event.key === 'Enter') {
710
- event.preventDefault()
711
- handleSaveFieldEdit()
712
- return
713
- }
714
-
715
- if (event.key === 'Escape') {
716
- event.preventDefault()
717
- handleCancelFieldEdit()
718
- }
719
- }
720
-
721
- const parseDataForViewer = (value) => {
722
- if (typeof value === 'string') {
723
- try {
724
- return JSON.parse(value)
725
- } catch {
726
- return null
727
- }
728
- }
729
-
730
- return value
731
- }
732
-
733
- const updateDataAtPath = (source, path, nextValue) => {
734
- if (path.length === 0) {
735
- return nextValue
736
- }
737
-
738
- const [segment, ...restPath] = path
739
-
740
- if (Array.isArray(source)) {
741
- const nextArray = [...source]
742
- nextArray[segment] = updateDataAtPath(nextArray[segment], restPath, nextValue)
743
- return nextArray
744
- }
745
-
746
- if (source !== null && typeof source === 'object') {
747
- return {
748
- ...source,
749
- [segment]: updateDataAtPath(source[segment], restPath, nextValue),
750
- }
751
- }
752
-
753
- return source
754
- }
755
-
756
- const handleUpdateDataNode = (path, nextValue) => {
757
- if (endpointIndex === null || endpoint.data === undefined) {
758
- return
759
- }
760
-
761
- const currentData = parseDataForViewer(endpoint.data)
762
- if (currentData === null) {
763
- return
764
- }
765
-
766
- const updatedData = updateDataAtPath(currentData, path, nextValue)
767
- onSaveField(endpointIndex, 'data', updatedData)
768
- }
769
-
770
- const deepClone = (value) => {
771
- if (typeof structuredClone === 'function') {
772
- return structuredClone(value)
773
- }
774
-
775
- return JSON.parse(JSON.stringify(value))
776
- }
777
-
778
- const getValueAtPath = (source, path) => {
779
- let currentValue = source
780
-
781
- for (const segment of path) {
782
- if (currentValue === null || typeof currentValue !== 'object') {
783
- return undefined
784
- }
785
-
786
- currentValue = currentValue[segment]
787
- }
788
-
789
- return currentValue
790
- }
791
-
792
- const buildDuplicatedObject = (parentObject, key, count, targetValue) => {
793
- const nextObject = {}
794
-
795
- for (const [entryKey, entryValue] of Object.entries(parentObject)) {
796
- nextObject[entryKey] = entryValue
797
-
798
- if (entryKey === key) {
799
- for (let index = 2; index <= count; index += 1) {
800
- let duplicateKey = `${key}_${index}`
801
- let suffix = 1
802
-
803
- while (Object.prototype.hasOwnProperty.call(nextObject, duplicateKey)) {
804
- duplicateKey = `${key}_${index}_${suffix}`
805
- suffix += 1
806
- }
807
-
808
- nextObject[duplicateKey] = deepClone(targetValue)
809
- }
810
- }
811
- }
812
-
813
- return nextObject
814
- }
815
-
816
- const handleDuplicateDataNode = (path, count) => {
817
- if (endpointIndex === null || endpoint.data === undefined || count <= 1) {
818
- return
819
- }
820
-
821
- const currentData = parseDataForViewer(endpoint.data)
822
- if (currentData === null) {
823
- return
824
- }
825
-
826
- if (path.length === 0) {
827
- const duplicatedRoot = Array.from({ length: count }, () => deepClone(currentData))
828
- onSaveField(endpointIndex, 'data', duplicatedRoot)
829
- return
830
- }
831
-
832
- const parentPath = path.slice(0, -1)
833
- const segment = path[path.length - 1]
834
- const parentValue = getValueAtPath(currentData, parentPath)
835
-
836
- if (Array.isArray(parentValue)) {
837
- const targetIndex = Number(segment)
838
- if (!Number.isInteger(targetIndex) || targetIndex < 0 || targetIndex >= parentValue.length) {
839
- return
840
- }
841
-
842
- const targetValue = parentValue[targetIndex]
843
- const duplicatedValues = Array.from({ length: count }, () => deepClone(targetValue))
844
- const nextParentArray = [
845
- ...parentValue.slice(0, targetIndex),
846
- ...duplicatedValues,
847
- ...parentValue.slice(targetIndex + 1),
848
- ]
849
- const updatedData = updateDataAtPath(currentData, parentPath, nextParentArray)
850
- onSaveField(endpointIndex, 'data', updatedData)
851
- return
852
- }
853
-
854
- if (parentValue !== null && typeof parentValue === 'object') {
855
- const targetKey = String(segment)
856
- if (!Object.prototype.hasOwnProperty.call(parentValue, targetKey)) {
857
- return
858
- }
859
-
860
- const targetValue = parentValue[targetKey]
861
- const nextParentObject = buildDuplicatedObject(parentValue, targetKey, count, targetValue)
862
- const updatedData = updateDataAtPath(currentData, parentPath, nextParentObject)
863
- onSaveField(endpointIndex, 'data', updatedData)
864
- }
865
- }
866
-
867
- const handleDeleteDataNode = (path) => {
868
- if (endpointIndex === null || endpoint.data === undefined || path.length === 0) {
869
- return
870
- }
871
-
872
- const currentData = parseDataForViewer(endpoint.data)
873
- if (currentData === null) {
874
- return
875
- }
876
-
877
- const parentPath = path.slice(0, -1)
878
- const segment = path[path.length - 1]
879
- const parentValue = getValueAtPath(currentData, parentPath)
880
-
881
- if (Array.isArray(parentValue)) {
882
- const targetIndex = Number(segment)
883
- if (!Number.isInteger(targetIndex) || targetIndex < 0 || targetIndex >= parentValue.length) {
884
- return
885
- }
886
-
887
- const nextParentArray = parentValue.filter((_, index) => index !== targetIndex)
888
- const updatedData = updateDataAtPath(currentData, parentPath, nextParentArray)
889
- onSaveField(endpointIndex, 'data', updatedData)
890
- return
891
- }
892
-
893
- if (parentValue !== null && typeof parentValue === 'object') {
894
- const targetKey = String(segment)
895
- if (!Object.prototype.hasOwnProperty.call(parentValue, targetKey)) {
896
- return
897
- }
898
-
899
- const nextParentObject = { ...parentValue }
900
- delete nextParentObject[targetKey]
901
- const updatedData = updateDataAtPath(currentData, parentPath, nextParentObject)
902
- onSaveField(endpointIndex, 'data', updatedData)
903
- }
904
- }
905
-
906
- const handleReorderDataNode = (path, direction) => {
907
- if (endpointIndex === null || endpoint.data === undefined || path.length === 0) {
908
- return
909
- }
910
-
911
- const currentData = parseDataForViewer(endpoint.data)
912
- if (currentData === null) {
913
- return
914
- }
915
-
916
- const parentPath = path.slice(0, -1)
917
- const segment = path[path.length - 1]
918
- const parentValue = getValueAtPath(currentData, parentPath)
919
-
920
- if (Array.isArray(parentValue)) {
921
- const currentIndex = Number(segment)
922
- if (!Number.isInteger(currentIndex)) {
923
- return
924
- }
925
-
926
- const targetIndex = direction === 'up' ? currentIndex - 1 : currentIndex + 1
927
- if (targetIndex < 0 || targetIndex >= parentValue.length) {
928
- return
929
- }
930
-
931
- const nextParentArray = [...parentValue]
932
- const tempValue = nextParentArray[currentIndex]
933
- nextParentArray[currentIndex] = nextParentArray[targetIndex]
934
- nextParentArray[targetIndex] = tempValue
935
-
936
- const updatedData = updateDataAtPath(currentData, parentPath, nextParentArray)
937
- onSaveField(endpointIndex, 'data', updatedData)
938
- return
939
- }
940
-
941
- if (parentValue !== null && typeof parentValue === 'object') {
942
- const keys = Object.keys(parentValue)
943
- const currentIndex = keys.indexOf(String(segment))
944
- if (currentIndex === -1) {
945
- return
946
- }
947
-
948
- const targetIndex = direction === 'up' ? currentIndex - 1 : currentIndex + 1
949
- if (targetIndex < 0 || targetIndex >= keys.length) {
950
- return
951
- }
952
-
953
- const nextKeys = [...keys]
954
- const tempKey = nextKeys[currentIndex]
955
- nextKeys[currentIndex] = nextKeys[targetIndex]
956
- nextKeys[targetIndex] = tempKey
957
-
958
- const nextParentObject = {}
959
- for (const key of nextKeys) {
960
- nextParentObject[key] = parentValue[key]
961
- }
962
-
963
- const updatedData = updateDataAtPath(currentData, parentPath, nextParentObject)
964
- onSaveField(endpointIndex, 'data', updatedData)
965
- }
966
- }
967
-
968
- if (!isOpen || endpointIndex === null || !endpoint) {
969
- return null
970
- }
971
-
972
- const parsedDataForViewer = endpoint.data !== undefined ? parseDataForViewer(endpoint.data) : null
973
- const modalContent = (
974
- <>
975
- <div className="fixed inset-0 z-40 bg-black/60 backdrop-blur-[2px]" onClick={handleCloseDrawer} />
976
- <div className="fixed inset-0 z-50 grid place-items-center p-3 md:p-6">
977
- <aside className="w-[94vw] max-w-5xl">
978
- <div className="flex h-[min(86vh,920px)] w-full flex-col rounded-xl border border-white/10 bg-[#0A0A0A] shadow-2xl overflow-hidden">
979
- <div className="flex items-center justify-between border-b border-white/5 bg-white/5 px-4 py-3">
980
- <div className="flex items-center gap-2">
981
- <h2 className="text-sm font-medium text-neutral-200">Dataset Configuration</h2>
982
- </div>
983
- <div className="flex items-center gap-2">
984
- <button
985
- onClick={() => setShowHelp(true)}
986
- className="flex items-center gap-2 rounded-lg p-1.5 text-neutral-400 transition-colors hover:bg-white/10 hover:text-white"
987
- title="Help Guide"
988
- >
989
- <span className="text-xs">How to Use</span>
990
- <HelpCircle className="h-4 w-4" />
991
- </button>
992
- <button
993
- onClick={handleCloseDrawer}
994
- className="rounded-lg p-1.5 text-neutral-400 transition-colors hover:bg-white/10 hover:text-white"
995
- >
996
- <svg className="h-4 w-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><line x1="18" y1="6" x2="6" y2="18"></line><line x1="6" y1="6" x2="18" y2="18"></line></svg>
997
- </button>
998
- </div>
999
- </div>
1000
-
1001
- <div className="min-h-0 flex-1 overflow-y-auto p-6 bg-[#0c0c0c]">
1002
- <div className="mb-6 inline-flex rounded border border-indigo-500/20 bg-indigo-500/10 px-3 py-1.5 text-xs font-mono text-indigo-400">
1003
- {endpoint.method} {endpoint.path}
1004
- </div>
1005
-
1006
- {endpoint.data !== undefined && (
1007
- <div className="mb-6">
1008
- <h3 className="mb-3 text-[11px] font-semibold uppercase tracking-wider text-neutral-500 flex items-center gap-2">
1009
- <svg className="h-3.5 w-3.5 text-indigo-400" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><ellipse cx="12" cy="5" rx="9" ry="3"></ellipse><path d="M21 12c0 1.66-4 3-9 3s-9-1.34-9-3"></path><path d="M3 5v14c0 1.66 4 3 9 3s9-1.34 9-3V5"></path></svg>
1010
- Response Data
1011
- </h3>
1012
- <div className={getPreviewClassName()} title="JSON dataset preview">
1013
- {parsedDataForViewer === null ? (
1014
- <div>
1015
- <p className="mb-2 text-sm text-red-400">Data is not valid JSON.</p>
1016
- <pre className="whitespace-pre-wrap wrap-break-word font-mono text-sm text-neutral-200">
1017
- {String(endpoint.data)}
1018
- </pre>
1019
- </div>
1020
- ) : (
1021
- <JsonTreeNode
1022
- label={null}
1023
- value={parsedDataForViewer}
1024
- path={[]}
1025
- onUpdateNode={handleUpdateDataNode}
1026
- onDuplicateNode={handleDuplicateDataNode}
1027
- onDeleteNode={handleDeleteDataNode}
1028
- onReorderNode={handleReorderDataNode}
1029
- />
1030
- )}
1031
- </div>
1032
- </div>
1033
- )}
1034
-
1035
- {endpoint.template !== undefined && (
1036
- <div className="mb-6">
1037
- <h3 className="mb-3 text-[11px] font-semibold uppercase tracking-wider text-neutral-500 flex items-center gap-2">
1038
- <svg className="h-3.5 w-3.5 text-emerald-400" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><polyline points="16 18 22 12 16 6"></polyline><polyline points="8 6 2 12 8 18"></polyline></svg>
1039
- Template Editor
1040
- </h3>
1041
- {editingField === 'template' ? (
1042
- <textarea
1043
- ref={editorRef}
1044
- autoFocus
1045
- value={editorValue}
1046
- onChange={(event) => setEditorValue(event.target.value)}
1047
- onBlur={handleSaveFieldEdit}
1048
- onKeyDown={handleEditorKeyDown}
1049
- className={getEditorClassName()}
1050
- />
1051
- ) : (
1052
- <pre
1053
- onDoubleClick={() => handleStartFieldEdit('template')}
1054
- className={getPreviewClassName() + ' hover:border-white/10 hover:bg-white/5 transition-colors'}
1055
- title="Double-click to edit"
1056
- >
1057
- {typeof endpoint.template === 'string'
1058
- ? endpoint.template
1059
- : JSON.stringify(endpoint.template, null, 2)}
1060
- </pre>
1061
- )}
1062
- </div>
1063
- )}
1064
-
1065
- {endpoint.data === undefined && endpoint.template === undefined && (
1066
- <div className="flex flex-col items-center justify-center py-12 text-center border border-dashed border-white/10 rounded-xl bg-white/5">
1067
- <svg className="h-8 w-8 text-neutral-500 mb-3" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"><circle cx="12" cy="12" r="10"></circle><line x1="12" y1="8" x2="12" y2="12"></line><line x1="12" y1="16" x2="12.01" y2="16"></line></svg>
1068
- <p className="text-sm font-medium text-neutral-300">No data configured</p>
1069
- <p className="text-xs text-neutral-500 mt-1">This endpoint has no data or template configuration.</p>
1070
- </div>
1071
- )}
1072
- </div>
1073
- </div>
1074
- </aside>
1075
-
1076
- <AnimatePresence>
1077
- {showHelp && (
1078
- <motion.div
1079
- initial={{ opacity: 0, scale: 0.95, y: 20 }}
1080
- animate={{ opacity: 1, scale: 1, y: 0 }}
1081
- exit={{ opacity: 0, scale: 0.95, y: 20 }}
1082
- transition={{ duration: 0.2 }}
1083
- className="absolute top-20 right-6 z-60 w-80 rounded-xl border border-white/10 bg-[#111] shadow-2xl p-5"
1084
- >
1085
- <div className="flex items-center justify-between mb-4">
1086
- <h3 className="text-sm font-semibold text-neutral-200">Dataset Editor Guide</h3>
1087
- <button
1088
- onClick={() => setShowHelp(false)}
1089
- className="text-neutral-500 hover:text-white transition-colors"
1090
- >
1091
- <X size={16} />
1092
- </button>
1093
- </div>
1094
-
1095
- <div className="space-y-4 text-[13px] text-neutral-400">
1096
- <div>
1097
- <h4 className="text-indigo-400 font-medium mb-1">Repetition Syntax</h4>
1098
- <p>Hover over any array or object closing bracket (<code className="text-neutral-300">]</code> or <code className="text-neutral-300">{`}`}</code>) to reveal the duplicate multiplier and multiply your mock data items seamlessly.</p>
1099
- </div>
1100
-
1101
- <div>
1102
- <h4 className="text-indigo-400 font-medium mb-1">Quick Add</h4>
1103
- <p>Hover over the opening bracket of an object to add new arrays/objects or quickly insert preset mock tokens like <code className="text-neutral-300">$name</code> or <code className="text-neutral-300">$uuid</code>.</p>
1104
- </div>
1105
-
1106
- <div>
1107
- <h4 className="text-indigo-400 font-medium mb-1">Edit Keys & Values</h4>
1108
- <p>Click on any key or straight-value inline to quickly edit them. Hit Enter or blur to save your changes immediately.</p>
1109
- </div>
1110
-
1111
- <div>
1112
- <h4 className="text-indigo-400 font-medium mb-1">Reversing Changes</h4>
1113
- <p>Hit the <code className="text-neutral-300">Edit</code> button to start modifying a large block or revert any unwanted items natively without restarting.</p>
1114
- </div>
1115
- </div>
1116
- </motion.div>
1117
- )}
1118
- </AnimatePresence>
1119
- </div>
1120
- </>
1121
- )
1122
-
1123
- return createPortal(modalContent, document.body)
1124
- }