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.
- package/package.json +1 -7
- package/src/frontend/build/assets/index-CAMetoZ-.js +18 -0
- package/src/frontend/build/assets/index-D3B5qM-B.css +1 -0
- package/src/frontend/{index.html → build/index.html} +2 -1
- package/src/libs/backend.js +1 -1
- package/src/frontend/README.md +0 -16
- package/src/frontend/eslint.config.js +0 -29
- package/src/frontend/package.json +0 -33
- package/src/frontend/src/App.jsx +0 -104
- package/src/frontend/src/components/ConfigEditor.jsx +0 -172
- package/src/frontend/src/components/ConfigVisualizer.jsx +0 -428
- package/src/frontend/src/components/DatasetEditor.jsx +0 -1124
- package/src/frontend/src/context/ConfigState.jsx +0 -220
- package/src/frontend/src/index.css +0 -6
- package/src/frontend/src/main.jsx +0 -10
- package/src/frontend/vite.config.js +0 -12
|
@@ -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
|
-
}
|