@safagayret/bemirror 1.0.0
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/LICENSE +21 -0
- package/README.md +85 -0
- package/bin/bemirror +20 -0
- package/endpoints.json +122 -0
- package/index.js +8 -0
- package/package.json +47 -0
- package/public/css/style.css +954 -0
- package/public/index.html +505 -0
- package/public/js/app.js +1008 -0
- package/server.js +322 -0
package/public/js/app.js
ADDED
|
@@ -0,0 +1,1008 @@
|
|
|
1
|
+
let state = { projects: [] }
|
|
2
|
+
let activeIds = { project: null, entity: null, endpoint: null }
|
|
3
|
+
let treeSearchQuery = ''
|
|
4
|
+
let editors = {
|
|
5
|
+
payload: null,
|
|
6
|
+
params: null,
|
|
7
|
+
response: null,
|
|
8
|
+
prodResp: null,
|
|
9
|
+
headers: null,
|
|
10
|
+
variables: null,
|
|
11
|
+
}
|
|
12
|
+
let variables = {}
|
|
13
|
+
|
|
14
|
+
let importBuffer = null
|
|
15
|
+
|
|
16
|
+
const ICON_PROJ = `<svg class="icon"><path d="M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-6l-2-2H5a2 2 0 00-2 2z"></path></svg>`
|
|
17
|
+
const ICON_ENT = `<svg class="icon"><path d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10"></path></svg>`
|
|
18
|
+
const ARROW = `<svg class="arrow"><path d="M9 5l7 7-7 7"></path></svg>`
|
|
19
|
+
const EXPORT_ICON = `<svg><path d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4"></path></svg>`
|
|
20
|
+
const TRASH_ICON = `<svg><path d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"></path></svg>`
|
|
21
|
+
const PLUS_ICON = `<svg><path d="M12 4v16m8-8H4"></path></svg>`
|
|
22
|
+
const EDIT_ICON = `<svg><path d="M15.232 5.232l3.536 3.536m-2.036-5.036a2.5 2.5 0 113.536 3.536L6.5 21.036H3v-3.572L16.732 3.732z"></path></svg>`
|
|
23
|
+
const LINK_ICON = `<svg><path d="M13.828 10.172a4 4 0 00-5.656 0l-4 4a4 4 0 105.656 5.656l1.102-1.101m-.758-4.899a4 4 0 005.656 0l4-4a4 4 0 00-5.656-5.656l-1.1 1.1"></path></svg>`
|
|
24
|
+
|
|
25
|
+
document.addEventListener('DOMContentLoaded', async () => {
|
|
26
|
+
initEditors()
|
|
27
|
+
bindGlobalEvents()
|
|
28
|
+
setupTabs()
|
|
29
|
+
await loadData()
|
|
30
|
+
|
|
31
|
+
// Show About modal on first visit
|
|
32
|
+
if (!localStorage.getItem('bemirror_about_shown')) {
|
|
33
|
+
document.getElementById('modalAbout').style.display = 'flex'
|
|
34
|
+
localStorage.setItem('bemirror_about_shown', 'true')
|
|
35
|
+
}
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
function initEditors() {
|
|
39
|
+
const commonAceConfig = {
|
|
40
|
+
theme: 'ace/theme/tomorrow_night_eighties',
|
|
41
|
+
mode: 'ace/mode/json',
|
|
42
|
+
tabSize: 2,
|
|
43
|
+
useSoftTabs: true,
|
|
44
|
+
showPrintMargin: false,
|
|
45
|
+
}
|
|
46
|
+
editors.payload = ace.edit('editorPayload', commonAceConfig)
|
|
47
|
+
editors.params = ace.edit('editorParams', commonAceConfig)
|
|
48
|
+
editors.headers = ace.edit('editorHeaders', commonAceConfig)
|
|
49
|
+
editors.variables = ace.edit('editorVariables', commonAceConfig)
|
|
50
|
+
editors.response = ace.edit('editorResponse', commonAceConfig)
|
|
51
|
+
editors.prodResp = ace.edit('editorProdResponse', commonAceConfig)
|
|
52
|
+
editors.prodResp.setReadOnly(true)
|
|
53
|
+
|
|
54
|
+
// Auto-Prettier Feature (JSON formatting on blur)
|
|
55
|
+
const formatJSON = (editor) => {
|
|
56
|
+
try {
|
|
57
|
+
const val = editor.getValue()
|
|
58
|
+
if (!val.trim()) return
|
|
59
|
+
const obj = JSON.parse(val)
|
|
60
|
+
editor.setValue(JSON.stringify(obj, null, 2), -1)
|
|
61
|
+
} catch (e) {
|
|
62
|
+
/* Ignore invalid JSON */
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
editors.payload.on('blur', () => formatJSON(editors.payload))
|
|
67
|
+
editors.params.on('blur', () => formatJSON(editors.params))
|
|
68
|
+
editors.headers.on('blur', () => formatJSON(editors.headers))
|
|
69
|
+
editors.variables.on('blur', () => formatJSON(editors.variables))
|
|
70
|
+
editors.response.on('blur', () => formatJSON(editors.response))
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function setupTabs() {
|
|
74
|
+
const btns = document.querySelectorAll('.tab-btn')
|
|
75
|
+
btns.forEach((btn) => {
|
|
76
|
+
btn.addEventListener('click', () => {
|
|
77
|
+
document.querySelectorAll('.tab-btn').forEach((b) => b.classList.remove('active'))
|
|
78
|
+
document.querySelectorAll('.tab-pane').forEach((p) => p.classList.remove('active'))
|
|
79
|
+
btn.classList.add('active')
|
|
80
|
+
const targetPane = document.getElementById(btn.getAttribute('data-tab'))
|
|
81
|
+
targetPane.classList.add('active')
|
|
82
|
+
|
|
83
|
+
// Fix ACE editor not rendering when container becomes visible
|
|
84
|
+
if (editors.params) editors.params.resize()
|
|
85
|
+
if (editors.payload) editors.payload.resize()
|
|
86
|
+
if (editors.headers) editors.headers.resize()
|
|
87
|
+
})
|
|
88
|
+
})
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const generateId = () => Math.random().toString(36).substr(2, 9)
|
|
92
|
+
const slugify = (str) => {
|
|
93
|
+
if (!str) return 'unknown'
|
|
94
|
+
return str
|
|
95
|
+
.toString()
|
|
96
|
+
.replace(/([a-z])([A-Z])/g, '$1-$2') // CamelCase / PascalCase to kebab-case
|
|
97
|
+
.toLowerCase()
|
|
98
|
+
.trim()
|
|
99
|
+
.replace(/[^\w\s-]/g, '')
|
|
100
|
+
.replace(/[\s_-]+/g, '-')
|
|
101
|
+
.replace(/^-+|-+$/g, '')
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function matchesTreeSearch(text) {
|
|
105
|
+
if (!treeSearchQuery) return true
|
|
106
|
+
return text.toString().toLowerCase().includes(treeSearchQuery)
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function getFilteredProjects() {
|
|
110
|
+
if (!treeSearchQuery) return state.projects
|
|
111
|
+
return state.projects
|
|
112
|
+
.map((proj) => {
|
|
113
|
+
const projectMatches = matchesTreeSearch(proj.name)
|
|
114
|
+
const entities = (proj.entities || [])
|
|
115
|
+
.map((ent) => {
|
|
116
|
+
const entityMatches = matchesTreeSearch(ent.name)
|
|
117
|
+
const endpoints = (ent.endpoints || []).filter((ep) => {
|
|
118
|
+
return matchesTreeSearch(ep.path) || matchesTreeSearch(ep.method || '')
|
|
119
|
+
})
|
|
120
|
+
if (entityMatches) {
|
|
121
|
+
return {
|
|
122
|
+
...ent,
|
|
123
|
+
endpoints: ent.endpoints || [],
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
if (endpoints.length) {
|
|
127
|
+
return {
|
|
128
|
+
...ent,
|
|
129
|
+
endpoints,
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
return null
|
|
133
|
+
})
|
|
134
|
+
.filter(Boolean)
|
|
135
|
+
|
|
136
|
+
if (projectMatches) {
|
|
137
|
+
return {
|
|
138
|
+
...proj,
|
|
139
|
+
entities: proj.entities || [],
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
if (entities.length) {
|
|
143
|
+
return {
|
|
144
|
+
...proj,
|
|
145
|
+
entities,
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
return null
|
|
149
|
+
})
|
|
150
|
+
.filter(Boolean)
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function getDemoProject() {
|
|
154
|
+
return {
|
|
155
|
+
projects: [
|
|
156
|
+
{
|
|
157
|
+
id: 'p-demo123',
|
|
158
|
+
name: 'Demo Project',
|
|
159
|
+
baseUrl: 'https://api.democorp.local',
|
|
160
|
+
entities: [
|
|
161
|
+
{
|
|
162
|
+
id: 'e-users',
|
|
163
|
+
name: 'Users',
|
|
164
|
+
endpoints: [
|
|
165
|
+
{
|
|
166
|
+
id: 'ep-u1',
|
|
167
|
+
method: 'GET',
|
|
168
|
+
path: '/profile',
|
|
169
|
+
statusCode: 200,
|
|
170
|
+
delay: 150,
|
|
171
|
+
expectedParams: '',
|
|
172
|
+
expectedPayload: '',
|
|
173
|
+
responseBody: JSON.stringify(
|
|
174
|
+
{ id: 1, name: 'John Doe', role: 'admin', status: 'active' },
|
|
175
|
+
null,
|
|
176
|
+
2,
|
|
177
|
+
),
|
|
178
|
+
},
|
|
179
|
+
{
|
|
180
|
+
id: 'ep-u2',
|
|
181
|
+
method: 'POST',
|
|
182
|
+
path: '/create',
|
|
183
|
+
statusCode: 201,
|
|
184
|
+
delay: 300,
|
|
185
|
+
expectedParams: '',
|
|
186
|
+
expectedPayload: JSON.stringify(
|
|
187
|
+
{ name: 'Jane', email: 'jane@example.com' },
|
|
188
|
+
null,
|
|
189
|
+
2,
|
|
190
|
+
),
|
|
191
|
+
responseBody: JSON.stringify(
|
|
192
|
+
{ success: true, userId: 2, message: 'User created' },
|
|
193
|
+
null,
|
|
194
|
+
2,
|
|
195
|
+
),
|
|
196
|
+
},
|
|
197
|
+
{
|
|
198
|
+
id: 'ep-u3',
|
|
199
|
+
method: 'DELETE',
|
|
200
|
+
path: '/1',
|
|
201
|
+
statusCode: 204,
|
|
202
|
+
delay: 100,
|
|
203
|
+
expectedParams: '',
|
|
204
|
+
expectedPayload: '',
|
|
205
|
+
responseBody: '',
|
|
206
|
+
},
|
|
207
|
+
],
|
|
208
|
+
},
|
|
209
|
+
{
|
|
210
|
+
id: 'e-posts',
|
|
211
|
+
name: 'Posts',
|
|
212
|
+
endpoints: [
|
|
213
|
+
{
|
|
214
|
+
id: 'ep-p1',
|
|
215
|
+
method: 'GET',
|
|
216
|
+
path: '/all',
|
|
217
|
+
statusCode: 200,
|
|
218
|
+
delay: 400,
|
|
219
|
+
expectedParams: JSON.stringify({ page: '1' }, null, 2),
|
|
220
|
+
expectedPayload: '',
|
|
221
|
+
responseBody: JSON.stringify(
|
|
222
|
+
{
|
|
223
|
+
data: [
|
|
224
|
+
{ id: 101, title: 'Hello World' },
|
|
225
|
+
{ id: 102, title: 'Mocking APIs' },
|
|
226
|
+
],
|
|
227
|
+
total: 2,
|
|
228
|
+
},
|
|
229
|
+
null,
|
|
230
|
+
2,
|
|
231
|
+
),
|
|
232
|
+
},
|
|
233
|
+
{
|
|
234
|
+
id: 'ep-p2',
|
|
235
|
+
method: 'PATCH',
|
|
236
|
+
path: '/101',
|
|
237
|
+
statusCode: 200,
|
|
238
|
+
delay: 200,
|
|
239
|
+
expectedParams: '',
|
|
240
|
+
expectedPayload: JSON.stringify({ title: 'Updated Title' }, null, 2),
|
|
241
|
+
responseBody: JSON.stringify(
|
|
242
|
+
{ id: 101, title: 'Updated Title', updatedAt: '2024-01-01T12:00:00Z' },
|
|
243
|
+
null,
|
|
244
|
+
2,
|
|
245
|
+
),
|
|
246
|
+
},
|
|
247
|
+
],
|
|
248
|
+
},
|
|
249
|
+
{
|
|
250
|
+
id: 'e-prods',
|
|
251
|
+
name: 'Products',
|
|
252
|
+
endpoints: [
|
|
253
|
+
{
|
|
254
|
+
id: 'ep-pr1',
|
|
255
|
+
method: 'GET',
|
|
256
|
+
path: '/search',
|
|
257
|
+
statusCode: 200,
|
|
258
|
+
delay: 350,
|
|
259
|
+
expectedParams: JSON.stringify({ q: 'laptop' }, null, 2),
|
|
260
|
+
expectedPayload: '',
|
|
261
|
+
responseBody: JSON.stringify(
|
|
262
|
+
{ results: [{ id: 'p-45', name: 'Pro Laptop', price: 1299.99 }] },
|
|
263
|
+
null,
|
|
264
|
+
2,
|
|
265
|
+
),
|
|
266
|
+
},
|
|
267
|
+
{
|
|
268
|
+
id: 'ep-pr2',
|
|
269
|
+
method: 'GET',
|
|
270
|
+
path: '/p-45',
|
|
271
|
+
statusCode: 200,
|
|
272
|
+
delay: 100,
|
|
273
|
+
expectedParams: '',
|
|
274
|
+
expectedPayload: '',
|
|
275
|
+
responseBody: JSON.stringify(
|
|
276
|
+
{
|
|
277
|
+
id: 'p-45',
|
|
278
|
+
name: 'Pro Laptop',
|
|
279
|
+
description: '16GB RAM, 512GB SSD',
|
|
280
|
+
inStock: true,
|
|
281
|
+
},
|
|
282
|
+
null,
|
|
283
|
+
2,
|
|
284
|
+
),
|
|
285
|
+
},
|
|
286
|
+
],
|
|
287
|
+
},
|
|
288
|
+
{
|
|
289
|
+
id: 'e-orders',
|
|
290
|
+
name: 'Orders',
|
|
291
|
+
endpoints: [
|
|
292
|
+
{
|
|
293
|
+
id: 'ep-o1',
|
|
294
|
+
method: 'POST',
|
|
295
|
+
path: '/checkout',
|
|
296
|
+
statusCode: 200,
|
|
297
|
+
delay: 600,
|
|
298
|
+
expectedParams: '',
|
|
299
|
+
expectedPayload: JSON.stringify({ productId: 'p-45', qty: 1 }, null, 2),
|
|
300
|
+
responseBody: JSON.stringify(
|
|
301
|
+
{ orderId: 'ORD-99812', status: 'processing', estimatedDelivery: '3 days' },
|
|
302
|
+
null,
|
|
303
|
+
2,
|
|
304
|
+
),
|
|
305
|
+
},
|
|
306
|
+
{
|
|
307
|
+
id: 'ep-o2',
|
|
308
|
+
method: 'GET',
|
|
309
|
+
path: '/ORD-99812/status',
|
|
310
|
+
statusCode: 200,
|
|
311
|
+
delay: 150,
|
|
312
|
+
expectedParams: '',
|
|
313
|
+
expectedPayload: '',
|
|
314
|
+
responseBody: JSON.stringify(
|
|
315
|
+
{ orderId: 'ORD-99812', status: 'shipped', tracking: 'UPS-12345' },
|
|
316
|
+
null,
|
|
317
|
+
2,
|
|
318
|
+
),
|
|
319
|
+
},
|
|
320
|
+
],
|
|
321
|
+
},
|
|
322
|
+
{
|
|
323
|
+
id: 'e-settings',
|
|
324
|
+
name: 'Settings',
|
|
325
|
+
endpoints: [
|
|
326
|
+
{
|
|
327
|
+
id: 'ep-s1',
|
|
328
|
+
method: 'GET',
|
|
329
|
+
path: '/preferences',
|
|
330
|
+
statusCode: 200,
|
|
331
|
+
delay: 50,
|
|
332
|
+
expectedParams: '',
|
|
333
|
+
expectedPayload: '',
|
|
334
|
+
responseBody: JSON.stringify(
|
|
335
|
+
{ theme: 'dark', notifications: true, language: 'en' },
|
|
336
|
+
null,
|
|
337
|
+
2,
|
|
338
|
+
),
|
|
339
|
+
},
|
|
340
|
+
{
|
|
341
|
+
id: 'ep-s2',
|
|
342
|
+
method: 'PUT',
|
|
343
|
+
path: '/preferences',
|
|
344
|
+
statusCode: 200,
|
|
345
|
+
delay: 250,
|
|
346
|
+
expectedParams: '',
|
|
347
|
+
expectedPayload: JSON.stringify({ theme: 'light' }, null, 2),
|
|
348
|
+
responseBody: JSON.stringify(
|
|
349
|
+
{ theme: 'light', notifications: true, language: 'en', updated: true },
|
|
350
|
+
null,
|
|
351
|
+
2,
|
|
352
|
+
),
|
|
353
|
+
},
|
|
354
|
+
],
|
|
355
|
+
},
|
|
356
|
+
],
|
|
357
|
+
},
|
|
358
|
+
],
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
function startOnboarding() {
|
|
363
|
+
if (window.driver) {
|
|
364
|
+
const driverObj = window.driver.js.driver({
|
|
365
|
+
showProgress: true,
|
|
366
|
+
steps: [
|
|
367
|
+
{
|
|
368
|
+
element: '#btnNewProject',
|
|
369
|
+
popover: {
|
|
370
|
+
title: 'Create a Project',
|
|
371
|
+
description: 'Start by creating your first mock API project here.',
|
|
372
|
+
side: 'right',
|
|
373
|
+
align: 'start',
|
|
374
|
+
},
|
|
375
|
+
},
|
|
376
|
+
{
|
|
377
|
+
element: '.sidebar',
|
|
378
|
+
popover: {
|
|
379
|
+
title: 'Organize APIs',
|
|
380
|
+
description:
|
|
381
|
+
'Your projects, entities, and endpoints will appear in this tree structure.',
|
|
382
|
+
side: 'right',
|
|
383
|
+
align: 'start',
|
|
384
|
+
},
|
|
385
|
+
},
|
|
386
|
+
{
|
|
387
|
+
element: '.sidebar-actions',
|
|
388
|
+
popover: {
|
|
389
|
+
title: 'Import & Export',
|
|
390
|
+
description: 'Share your mock data with teammates easily using these global actions.',
|
|
391
|
+
side: 'bottom',
|
|
392
|
+
align: 'start',
|
|
393
|
+
},
|
|
394
|
+
},
|
|
395
|
+
{
|
|
396
|
+
element: '#btnVariables',
|
|
397
|
+
popover: {
|
|
398
|
+
title: 'Manage Variables',
|
|
399
|
+
description:
|
|
400
|
+
'Define global variables like base URLs or credentials for use in your endpoints.',
|
|
401
|
+
side: 'bottom',
|
|
402
|
+
align: 'start',
|
|
403
|
+
},
|
|
404
|
+
},
|
|
405
|
+
],
|
|
406
|
+
onDestroyStarted: () => {
|
|
407
|
+
if (!driverObj.hasNextStep() || confirm('Are you sure you want to skip the tour?')) {
|
|
408
|
+
localStorage.setItem('bemirror_tour_done', 'true')
|
|
409
|
+
driverObj.destroy()
|
|
410
|
+
}
|
|
411
|
+
},
|
|
412
|
+
})
|
|
413
|
+
driverObj.drive()
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
async function loadData() {
|
|
418
|
+
try {
|
|
419
|
+
const response = await fetch('/api/data')
|
|
420
|
+
const serverData = await response.json()
|
|
421
|
+
state = serverData && serverData.projects ? serverData : { projects: [] }
|
|
422
|
+
|
|
423
|
+
// Load variables
|
|
424
|
+
const varsResponse = await fetch('/api/variables')
|
|
425
|
+
variables = await varsResponse.json()
|
|
426
|
+
|
|
427
|
+
renderTree()
|
|
428
|
+
updateMainPanel()
|
|
429
|
+
} catch (e) {
|
|
430
|
+
document.getElementById('projectTree').innerHTML =
|
|
431
|
+
`<div class="loading-state text-danger">Data Load Error</div>`
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
async function syncData() {
|
|
436
|
+
try {
|
|
437
|
+
await fetch('/api/data', {
|
|
438
|
+
method: 'POST',
|
|
439
|
+
headers: { 'Content-Type': 'application/json' },
|
|
440
|
+
body: JSON.stringify(state),
|
|
441
|
+
})
|
|
442
|
+
} catch (e) {
|
|
443
|
+
console.error('Failed to sync', e)
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
async function syncVariables() {
|
|
448
|
+
try {
|
|
449
|
+
await fetch('/api/variables', {
|
|
450
|
+
method: 'POST',
|
|
451
|
+
headers: { 'Content-Type': 'application/json' },
|
|
452
|
+
body: JSON.stringify(variables),
|
|
453
|
+
})
|
|
454
|
+
} catch (e) {
|
|
455
|
+
console.error('Failed to sync variables', e)
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
function triggerDownload(dataObj, filename) {
|
|
460
|
+
const dataStr =
|
|
461
|
+
'data:text/json;charset=utf-8,' + encodeURIComponent(JSON.stringify(dataObj, null, 2))
|
|
462
|
+
const a = document.createElement('a')
|
|
463
|
+
a.href = dataStr
|
|
464
|
+
a.download = filename
|
|
465
|
+
document.body.appendChild(a)
|
|
466
|
+
a.click()
|
|
467
|
+
a.remove()
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
window.exportSpecificNode = (type, projId, entId) => {
|
|
471
|
+
const p = JSON.parse(JSON.stringify(state.projects.find((x) => x.id === projId)))
|
|
472
|
+
if (!p) return
|
|
473
|
+
if (type === 'project') {
|
|
474
|
+
triggerDownload({ projects: [p] }, `bemirror_project_${slugify(p.name)}.json`)
|
|
475
|
+
} else if (type === 'entity') {
|
|
476
|
+
const e = p.entities.find((x) => x.id === entId)
|
|
477
|
+
if (!e) return
|
|
478
|
+
p.entities = [e]
|
|
479
|
+
triggerDownload({ projects: [p] }, `bemirror_${slugify(p.name)}_${slugify(e.name)}.json`)
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
window.renameNode = async (type, projId, entId) => {
|
|
484
|
+
const p = state.projects.find((x) => x.id === projId)
|
|
485
|
+
if (type === 'project') {
|
|
486
|
+
const newName = prompt(`Rename Project '${p.name}':`, p.name)
|
|
487
|
+
if (newName && newName.trim() !== p.name) {
|
|
488
|
+
p.name = newName.trim()
|
|
489
|
+
await syncData()
|
|
490
|
+
renderTree()
|
|
491
|
+
}
|
|
492
|
+
} else if (type === 'entity') {
|
|
493
|
+
const e = p.entities.find((x) => x.id === entId)
|
|
494
|
+
if (!e) return
|
|
495
|
+
const newName = prompt(`Rename Entity '${e.name}':`, e.name)
|
|
496
|
+
if (newName && newName.trim() !== e.name) {
|
|
497
|
+
e.name = newName.trim()
|
|
498
|
+
await syncData()
|
|
499
|
+
renderTree()
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
window.setBaseUrl = async (projId) => {
|
|
505
|
+
const p = state.projects.find((x) => x.id === projId)
|
|
506
|
+
const newUrl = prompt(
|
|
507
|
+
`Set Prod Base URL for Project '${p.name}'\nExample: https://api.production.com\nLeave empty to clear:`,
|
|
508
|
+
p.baseUrl || '',
|
|
509
|
+
)
|
|
510
|
+
if (newUrl !== null) {
|
|
511
|
+
p.baseUrl = newUrl.trim()
|
|
512
|
+
await syncData()
|
|
513
|
+
if (activeIds.project === projId) openEndpointEditor() // Refresh UI if active
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
function renderTree() {
|
|
518
|
+
const treeContainer = document.getElementById('projectTree')
|
|
519
|
+
if (state.projects.length === 0) {
|
|
520
|
+
treeContainer.innerHTML = `<div class="loading-state">No projects yet.<br>Click the + icon to start.</div>`
|
|
521
|
+
return
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
const visibleProjects = getFilteredProjects()
|
|
525
|
+
if (visibleProjects.length === 0) {
|
|
526
|
+
treeContainer.innerHTML = `<div class="loading-state">No matching projects, entities, or endpoints.</div>`
|
|
527
|
+
return
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
let html = ''
|
|
531
|
+
visibleProjects.forEach((proj) => {
|
|
532
|
+
html += `
|
|
533
|
+
<div class="tree-node expanded" data-type="project" data-id="${proj.id}">
|
|
534
|
+
<div class="tree-row">
|
|
535
|
+
${ARROW} ${ICON_PROJ} <span class="tree-label">${proj.name}</span>
|
|
536
|
+
<div class="tree-actions">
|
|
537
|
+
<button class="btn-icon" title="Edit Base URL" onclick="event.stopPropagation(); setBaseUrl('${proj.id}')">${LINK_ICON}</button>
|
|
538
|
+
<button class="btn-icon" title="Rename Project" onclick="event.stopPropagation(); renameNode('project', '${proj.id}')">${EDIT_ICON}</button>
|
|
539
|
+
<button class="btn-icon" title="Export Project" onclick="event.stopPropagation(); exportSpecificNode('project', '${proj.id}')">${EXPORT_ICON}</button>
|
|
540
|
+
<button class="btn-icon" title="Add Entity" onclick="event.stopPropagation(); addEntity('${proj.id}')">${PLUS_ICON}</button>
|
|
541
|
+
<button class="btn-icon text-danger" title="Delete Project" onclick="event.stopPropagation(); deleteNode('project', '${proj.id}')">${TRASH_ICON}</button>
|
|
542
|
+
</div>
|
|
543
|
+
</div>
|
|
544
|
+
<div class="tree-children">
|
|
545
|
+
`
|
|
546
|
+
;(proj.entities || []).forEach((ent) => {
|
|
547
|
+
html += `
|
|
548
|
+
<div class="tree-node expanded" data-type="entity" data-id="${ent.id}">
|
|
549
|
+
<div class="tree-row">
|
|
550
|
+
${ARROW} ${ICON_ENT} <span class="tree-label">${ent.name}</span>
|
|
551
|
+
<div class="tree-actions">
|
|
552
|
+
<button class="btn-icon" title="Rename Entity" onclick="event.stopPropagation(); renameNode('entity', '${proj.id}', '${ent.id}')">${EDIT_ICON}</button>
|
|
553
|
+
<button class="btn-icon" title="Export Entity" onclick="event.stopPropagation(); exportSpecificNode('entity', '${proj.id}', '${ent.id}')">${EXPORT_ICON}</button>
|
|
554
|
+
<button class="btn-icon" title="Add Endpoint" onclick="event.stopPropagation(); addEndpoint('${proj.id}', '${ent.id}')">${PLUS_ICON}</button>
|
|
555
|
+
<button class="btn-icon text-danger" title="Delete Entity" onclick="event.stopPropagation(); deleteNode('entity', '${proj.id}', '${ent.id}')">${TRASH_ICON}</button>
|
|
556
|
+
</div>
|
|
557
|
+
</div>
|
|
558
|
+
<div class="tree-children">
|
|
559
|
+
`
|
|
560
|
+
;(ent.endpoints || []).forEach((ep) => {
|
|
561
|
+
const isActive = activeIds.endpoint === ep.id ? 'active' : ''
|
|
562
|
+
html += `
|
|
563
|
+
<div class="tree-node" data-type="endpoint">
|
|
564
|
+
<div class="tree-row ${isActive}" onclick="event.stopPropagation(); selectEndpoint('${proj.id}', '${ent.id}', '${ep.id}')">
|
|
565
|
+
<span class="no-arrow"></span>
|
|
566
|
+
<span class="method-badge ${ep.method.toLowerCase()}">${ep.method}</span>
|
|
567
|
+
<span class="tree-label">${ep.path}</span>
|
|
568
|
+
</div>
|
|
569
|
+
</div>
|
|
570
|
+
`
|
|
571
|
+
})
|
|
572
|
+
html += `</div></div>`
|
|
573
|
+
})
|
|
574
|
+
html += `</div></div>`
|
|
575
|
+
})
|
|
576
|
+
treeContainer.innerHTML = html
|
|
577
|
+
|
|
578
|
+
document.querySelectorAll('.tree-row').forEach((row) => {
|
|
579
|
+
row.addEventListener('click', function () {
|
|
580
|
+
const node = this.closest('.tree-node')
|
|
581
|
+
if (node.dataset.type !== 'endpoint') node.classList.toggle('expanded')
|
|
582
|
+
})
|
|
583
|
+
})
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
function updateMainPanel() {
|
|
587
|
+
const ws = document.getElementById('welcomeScreen')
|
|
588
|
+
const es = document.getElementById('editorScreen')
|
|
589
|
+
if (!activeIds.endpoint) {
|
|
590
|
+
ws.style.display = 'flex'
|
|
591
|
+
es.style.display = 'none'
|
|
592
|
+
} else {
|
|
593
|
+
ws.style.display = 'none'
|
|
594
|
+
es.style.display = 'flex'
|
|
595
|
+
openEndpointEditor()
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
// CRUD
|
|
600
|
+
window.addEntity = async (projectId) => {
|
|
601
|
+
const name = prompt('Enter Entity Name (e.g. Settings):')
|
|
602
|
+
if (!name) return
|
|
603
|
+
const proj = state.projects.find((p) => p.id === projectId)
|
|
604
|
+
if (!proj.entities) proj.entities = []
|
|
605
|
+
proj.entities.push({ id: generateId(), name, endpoints: [] })
|
|
606
|
+
await syncData()
|
|
607
|
+
renderTree()
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
window.addEndpoint = async (projectId, entityId) => {
|
|
611
|
+
const proj = state.projects.find((p) => p.id === projectId)
|
|
612
|
+
const ent = proj.entities.find((e) => e.id === entityId)
|
|
613
|
+
if (!ent.endpoints) ent.endpoints = []
|
|
614
|
+
const ep = {
|
|
615
|
+
id: generateId(),
|
|
616
|
+
method: 'GET',
|
|
617
|
+
path: '/new',
|
|
618
|
+
statusCode: 200,
|
|
619
|
+
delay: 0,
|
|
620
|
+
expectedParams: '',
|
|
621
|
+
expectedPayload: '',
|
|
622
|
+
responseBody: '{"success": true}',
|
|
623
|
+
}
|
|
624
|
+
ent.endpoints.push(ep)
|
|
625
|
+
await syncData()
|
|
626
|
+
activeIds = { project: projectId, entity: entityId, endpoint: ep.id }
|
|
627
|
+
renderTree()
|
|
628
|
+
updateMainPanel()
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
window.deleteNode = async (type, projId, entId) => {
|
|
632
|
+
if (!confirm(`Are you sure you want to delete this ${type}?`)) return
|
|
633
|
+
const projIndex = state.projects.findIndex((p) => p.id === projId)
|
|
634
|
+
if (type === 'project') {
|
|
635
|
+
state.projects.splice(projIndex, 1)
|
|
636
|
+
if (activeIds.project === projId) activeIds.endpoint = null
|
|
637
|
+
} else if (type === 'entity') {
|
|
638
|
+
state.projects[projIndex].entities = state.projects[projIndex].entities.filter(
|
|
639
|
+
(e) => e.id !== entId,
|
|
640
|
+
)
|
|
641
|
+
if (activeIds.entity === entId) activeIds.endpoint = null
|
|
642
|
+
}
|
|
643
|
+
await syncData()
|
|
644
|
+
renderTree()
|
|
645
|
+
updateMainPanel()
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
window.selectEndpoint = (projId, entId, epId) => {
|
|
649
|
+
activeIds = { project: projId, entity: entId, endpoint: epId }
|
|
650
|
+
renderTree()
|
|
651
|
+
updateMainPanel()
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
const getActiveEndpointData = () =>
|
|
655
|
+
state.projects
|
|
656
|
+
.find((p) => p.id === activeIds.project)
|
|
657
|
+
?.entities.find((e) => e.id === activeIds.entity)
|
|
658
|
+
?.endpoints.find((ep) => ep.id === activeIds.endpoint)
|
|
659
|
+
|
|
660
|
+
function computeLiveUrl(proj, ent, epObj) {
|
|
661
|
+
const base = `${window.location.origin}/mock/${slugify(proj.name)}/${slugify(ent.name)}`
|
|
662
|
+
const finalPath = epObj.path.startsWith('/') ? epObj.path : '/' + epObj.path
|
|
663
|
+
return base + finalPath
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
function openEndpointEditor() {
|
|
667
|
+
const ep = getActiveEndpointData()
|
|
668
|
+
const p = state.projects.find((p) => p.id === activeIds.project)
|
|
669
|
+
const e = p.entities.find((e) => e.id === activeIds.entity)
|
|
670
|
+
if (!ep) return
|
|
671
|
+
|
|
672
|
+
document.getElementById('editorBreadcrumbs').innerText = `${p.name} > ${e.name}`
|
|
673
|
+
|
|
674
|
+
const fullUrl = computeLiveUrl(p, e, ep)
|
|
675
|
+
document.getElementById('liveUrlLink').innerText = fullUrl
|
|
676
|
+
document.getElementById('liveUrlLink').href = fullUrl
|
|
677
|
+
|
|
678
|
+
// Prod URL Auto-Assignment based on Base URL
|
|
679
|
+
const bUrl = (p.baseUrl || '').replace(/\/$/, '')
|
|
680
|
+
const fPath = ep.path.startsWith('/') ? ep.path : '/' + ep.path
|
|
681
|
+
const eSlug = slugify(e.name)
|
|
682
|
+
document.getElementById('prodUrlInput').value = bUrl ? `${bUrl}/${eSlug}${fPath}` : ''
|
|
683
|
+
|
|
684
|
+
document.getElementById('epId').value = ep.id
|
|
685
|
+
document.getElementById('method').value = ep.method
|
|
686
|
+
document.getElementById('path').value = ep.path
|
|
687
|
+
document.getElementById('statusCode').value = ep.statusCode || 200
|
|
688
|
+
document.getElementById('delay').value = ep.delay || 0
|
|
689
|
+
|
|
690
|
+
editors.payload.setValue(ep.expectedPayload || '', -1)
|
|
691
|
+
editors.params.setValue(ep.expectedParams || '', -1)
|
|
692
|
+
editors.headers.setValue(JSON.stringify(ep.headers || [], null, 2), -1)
|
|
693
|
+
editors.response.setValue(ep.responseBody || '', -1)
|
|
694
|
+
|
|
695
|
+
// Populate authorization
|
|
696
|
+
const auth = ep.authorization || { type: 'None', value: '' }
|
|
697
|
+
document.getElementById('authType').value = auth.type
|
|
698
|
+
document.getElementById('authValue').value = auth.value
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
document.getElementById('endpointForm').addEventListener('submit', async (ev) => {
|
|
702
|
+
ev.preventDefault()
|
|
703
|
+
const ep = getActiveEndpointData()
|
|
704
|
+
const p = state.projects.find((p) => p.id === activeIds.project)
|
|
705
|
+
const e = p.entities.find((e) => e.id === activeIds.entity)
|
|
706
|
+
|
|
707
|
+
ep.method = document.getElementById('method').value
|
|
708
|
+
let rawPath = document.getElementById('path').value
|
|
709
|
+
if (!rawPath.startsWith('/')) rawPath = '/' + rawPath
|
|
710
|
+
ep.path = rawPath
|
|
711
|
+
ep.statusCode = parseInt(document.getElementById('statusCode').value, 10)
|
|
712
|
+
ep.delay = parseInt(document.getElementById('delay').value, 10)
|
|
713
|
+
ep.expectedPayload = editors.payload.getValue()
|
|
714
|
+
ep.expectedParams = editors.params.getValue()
|
|
715
|
+
ep.responseBody = editors.response.getValue()
|
|
716
|
+
|
|
717
|
+
// Save authorization
|
|
718
|
+
ep.authorization = {
|
|
719
|
+
type: document.getElementById('authType').value,
|
|
720
|
+
value: document.getElementById('authValue').value,
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
// Save headers
|
|
724
|
+
try {
|
|
725
|
+
ep.headers = JSON.parse(editors.headers.getValue() || '[]')
|
|
726
|
+
} catch (e) {
|
|
727
|
+
ep.headers = []
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
const fullUrl = computeLiveUrl(p, e, ep)
|
|
731
|
+
document.getElementById('liveUrlLink').innerText = fullUrl
|
|
732
|
+
document.getElementById('liveUrlLink').href = fullUrl
|
|
733
|
+
|
|
734
|
+
const bUrl = (p.baseUrl || '').replace(/\/$/, '')
|
|
735
|
+
const eSlug = slugify(e.name)
|
|
736
|
+
document.getElementById('prodUrlInput').value = bUrl ? `${bUrl}/${eSlug}${ep.path}` : ''
|
|
737
|
+
|
|
738
|
+
const btn = document.getElementById('saveEndpointBtn')
|
|
739
|
+
const originalHtml = btn.innerHTML
|
|
740
|
+
btn.innerHTML = '✓ Saved Successfully!'
|
|
741
|
+
btn.style.background = 'var(--success)'
|
|
742
|
+
await syncData()
|
|
743
|
+
renderTree()
|
|
744
|
+
setTimeout(() => {
|
|
745
|
+
btn.innerHTML = originalHtml
|
|
746
|
+
btn.style.background = 'var(--accent)'
|
|
747
|
+
}, 1500)
|
|
748
|
+
})
|
|
749
|
+
|
|
750
|
+
document.getElementById('btnDeleteEndpoint').addEventListener('click', async () => {
|
|
751
|
+
if (!confirm('Are you sure you want to delete this endpoint?')) return
|
|
752
|
+
const proj = state.projects.find((p) => p.id === activeIds.project)
|
|
753
|
+
const ent = proj.entities.find((e) => e.id === activeIds.entity)
|
|
754
|
+
ent.endpoints = ent.endpoints.filter((ep) => ep.id !== activeIds.endpoint)
|
|
755
|
+
activeIds.endpoint = null
|
|
756
|
+
await syncData()
|
|
757
|
+
renderTree()
|
|
758
|
+
updateMainPanel()
|
|
759
|
+
})
|
|
760
|
+
|
|
761
|
+
// Global Buttons & Modal Handling
|
|
762
|
+
function bindGlobalEvents() {
|
|
763
|
+
const treeSearch = document.getElementById('treeSearch')
|
|
764
|
+
if (treeSearch) {
|
|
765
|
+
treeSearch.addEventListener('input', (e) => {
|
|
766
|
+
treeSearchQuery = e.target.value.trim().toLowerCase()
|
|
767
|
+
renderTree()
|
|
768
|
+
})
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
// Global Shortcuts
|
|
772
|
+
document.addEventListener('keydown', (e) => {
|
|
773
|
+
if ((e.ctrlKey || e.metaKey) && e.key === 'Enter') {
|
|
774
|
+
const editorScreen = document.getElementById('editorScreen')
|
|
775
|
+
if (editorScreen && editorScreen.style.display !== 'none') {
|
|
776
|
+
e.preventDefault()
|
|
777
|
+
document.getElementById('saveEndpointBtn').click()
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
})
|
|
781
|
+
|
|
782
|
+
// Mobile menu toggle
|
|
783
|
+
const mobileMenuBtn = document.getElementById('btnMobileMenu')
|
|
784
|
+
if (mobileMenuBtn) {
|
|
785
|
+
mobileMenuBtn.addEventListener('click', () => {
|
|
786
|
+
document.querySelector('.sidebar').classList.toggle('mobile-open')
|
|
787
|
+
})
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
document.getElementById('btnNewProject').addEventListener('click', async () => {
|
|
791
|
+
const name = prompt('Project Name (e.g. My Next API):')
|
|
792
|
+
if (!name) return
|
|
793
|
+
state.projects.push({ id: `p-${generateId()}`, name, entities: [] })
|
|
794
|
+
await syncData()
|
|
795
|
+
renderTree()
|
|
796
|
+
})
|
|
797
|
+
document
|
|
798
|
+
.getElementById('btnCreateFirstProject')
|
|
799
|
+
.addEventListener('click', () => document.getElementById('btnNewProject').click())
|
|
800
|
+
document.getElementById('btnExport').addEventListener('click', () => {
|
|
801
|
+
document.getElementById('modalExport').style.display = 'flex'
|
|
802
|
+
})
|
|
803
|
+
|
|
804
|
+
document.getElementById('btnExportDownload').addEventListener('click', () => {
|
|
805
|
+
triggerDownload(state, 'bemirror_export_fully.json')
|
|
806
|
+
document.getElementById('modalExport').style.display = 'none'
|
|
807
|
+
})
|
|
808
|
+
|
|
809
|
+
document.getElementById('btnExportCopy').addEventListener('click', () => {
|
|
810
|
+
navigator.clipboard.writeText(JSON.stringify(state, null, 2)).then(() => {
|
|
811
|
+
const btn = document.getElementById('btnExportCopy')
|
|
812
|
+
const oldText = btn.innerHTML
|
|
813
|
+
btn.innerHTML = '✓ Copied!'
|
|
814
|
+
setTimeout(() => {
|
|
815
|
+
btn.innerHTML = oldText
|
|
816
|
+
document.getElementById('modalExport').style.display = 'none'
|
|
817
|
+
}, 1500)
|
|
818
|
+
})
|
|
819
|
+
})
|
|
820
|
+
|
|
821
|
+
document.getElementById('btnAbout').addEventListener('click', () => {
|
|
822
|
+
document.getElementById('modalAbout').style.display = 'flex'
|
|
823
|
+
})
|
|
824
|
+
|
|
825
|
+
document.getElementById('btnVariables').addEventListener('click', () => {
|
|
826
|
+
editors.variables.setValue(JSON.stringify(variables, null, 2), -1)
|
|
827
|
+
document.getElementById('modalVariables').style.display = 'flex'
|
|
828
|
+
})
|
|
829
|
+
|
|
830
|
+
document.getElementById('btnSaveVariables').addEventListener('click', async () => {
|
|
831
|
+
try {
|
|
832
|
+
const newVars = JSON.parse(editors.variables.getValue())
|
|
833
|
+
variables = newVars
|
|
834
|
+
await syncVariables()
|
|
835
|
+
document.getElementById('modalVariables').style.display = 'none'
|
|
836
|
+
alert('Variables saved successfully!')
|
|
837
|
+
} catch (e) {
|
|
838
|
+
alert('Invalid JSON format. Please check your variables.')
|
|
839
|
+
}
|
|
840
|
+
})
|
|
841
|
+
|
|
842
|
+
document.getElementById('btnImport').addEventListener('click', () => {
|
|
843
|
+
document.getElementById('importPasteArea').value = ''
|
|
844
|
+
document.getElementById('modalImport').style.display = 'flex'
|
|
845
|
+
})
|
|
846
|
+
|
|
847
|
+
document.getElementById('fileImportAdvanced').addEventListener('change', (e) => {
|
|
848
|
+
const file = e.target.files[0]
|
|
849
|
+
if (!file) {
|
|
850
|
+
document.getElementById('fileImportStatus').innerText = 'No file selected.'
|
|
851
|
+
return
|
|
852
|
+
}
|
|
853
|
+
document.getElementById('fileImportStatus').innerText = `Selected: ${file.name}`
|
|
854
|
+
const reader = new FileReader()
|
|
855
|
+
reader.onload = (ev) => {
|
|
856
|
+
try {
|
|
857
|
+
const imported = JSON.parse(ev.target.result)
|
|
858
|
+
if (!imported.projects) throw new Error('Missing projects array')
|
|
859
|
+
importBuffer = imported.projects
|
|
860
|
+
} catch (err) {
|
|
861
|
+
alert('Invalid JSON format. Check your import file.')
|
|
862
|
+
importBuffer = null
|
|
863
|
+
document.getElementById('fileImportStatus').innerText = 'Invalid file.'
|
|
864
|
+
}
|
|
865
|
+
}
|
|
866
|
+
reader.readAsText(file)
|
|
867
|
+
})
|
|
868
|
+
|
|
869
|
+
document.getElementById('btnExecuteImport').addEventListener('click', async () => {
|
|
870
|
+
const pasteVal = document.getElementById('importPasteArea').value.trim()
|
|
871
|
+
if (pasteVal) {
|
|
872
|
+
try {
|
|
873
|
+
const parsed = JSON.parse(pasteVal)
|
|
874
|
+
if (parsed.projects) {
|
|
875
|
+
importBuffer = parsed.projects
|
|
876
|
+
} else {
|
|
877
|
+
throw new Error('Missing projects array in pasted JSON')
|
|
878
|
+
}
|
|
879
|
+
} catch (e) {
|
|
880
|
+
return alert('Invalid pasted JSON format.')
|
|
881
|
+
}
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
if (!importBuffer) {
|
|
885
|
+
return alert('Please upload a valid JSON file or paste valid JSON code.')
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
const strategy = document.querySelector('input[name="importStrategy"]:checked').value
|
|
889
|
+
if (strategy === 'overwrite') {
|
|
890
|
+
state.projects = importBuffer
|
|
891
|
+
} else {
|
|
892
|
+
state.projects = [...state.projects, ...importBuffer]
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
await syncData()
|
|
896
|
+
renderTree()
|
|
897
|
+
updateMainPanel()
|
|
898
|
+
document.getElementById('modalImport').style.display = 'none'
|
|
899
|
+
importBuffer = null
|
|
900
|
+
document.getElementById('fileImportAdvanced').value = ''
|
|
901
|
+
document.getElementById('fileImportStatus').innerText = 'No file selected.'
|
|
902
|
+
document.getElementById('importPasteArea').value = ''
|
|
903
|
+
})
|
|
904
|
+
|
|
905
|
+
// Export as cURL command
|
|
906
|
+
document.getElementById('btnCopyCurl').addEventListener('click', () => {
|
|
907
|
+
const ep = getActiveEndpointData()
|
|
908
|
+
if (!ep) return
|
|
909
|
+
const p = state.projects.find((p) => p.id === activeIds.project)
|
|
910
|
+
const e = p.entities.find((e) => e.id === activeIds.entity)
|
|
911
|
+
|
|
912
|
+
let finalUrl = computeLiveUrl(p, e, ep)
|
|
913
|
+
if (ep.expectedParams) {
|
|
914
|
+
try {
|
|
915
|
+
const pg = JSON.parse(ep.expectedParams)
|
|
916
|
+
const pStr = new URLSearchParams(pg).toString()
|
|
917
|
+
if (pStr) finalUrl += '?' + pStr
|
|
918
|
+
} catch (err) {}
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
let cmd = `curl -X ${ep.method} "${finalUrl}"`
|
|
922
|
+
if (ep.expectedPayload && ['POST', 'PUT', 'PATCH'].includes(ep.method)) {
|
|
923
|
+
cmd += ` \\\n -H "Content-Type: application/json"`
|
|
924
|
+
cmd += ` \\\n -d '${ep.expectedPayload.trim().replace(/'/g, "'\\''")}'`
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
navigator.clipboard.writeText(cmd).then(() => {
|
|
928
|
+
const btn = document.getElementById('btnCopyCurl')
|
|
929
|
+
btn.innerText = '✓ Copied!'
|
|
930
|
+
setTimeout(() => (btn.innerText = 'Copy cURL'), 2000)
|
|
931
|
+
})
|
|
932
|
+
})
|
|
933
|
+
|
|
934
|
+
// Export as JS fetch command
|
|
935
|
+
document.getElementById('btnCopyFetch').addEventListener('click', () => {
|
|
936
|
+
const ep = getActiveEndpointData()
|
|
937
|
+
if (!ep) return
|
|
938
|
+
const p = state.projects.find((p) => p.id === activeIds.project)
|
|
939
|
+
const e = p.entities.find((e) => e.id === activeIds.entity)
|
|
940
|
+
|
|
941
|
+
let finalUrl = computeLiveUrl(p, e, ep)
|
|
942
|
+
if (ep.expectedParams) {
|
|
943
|
+
try {
|
|
944
|
+
const pg = JSON.parse(ep.expectedParams)
|
|
945
|
+
const pStr = new URLSearchParams(pg).toString()
|
|
946
|
+
if (pStr) finalUrl += '?' + pStr
|
|
947
|
+
} catch (err) {}
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
let cmd = `fetch("${finalUrl}", {\n method: "${ep.method}"`
|
|
951
|
+
|
|
952
|
+
if (ep.expectedPayload && ['POST', 'PUT', 'PATCH'].includes(ep.method)) {
|
|
953
|
+
cmd += `,\n headers: {\n "Content-Type": "application/json"\n },\n`
|
|
954
|
+
let safePayload = ep.expectedPayload.trim()
|
|
955
|
+
if (!safePayload) safePayload = '{}'
|
|
956
|
+
cmd += ` body: JSON.stringify(${safePayload})\n`
|
|
957
|
+
} else {
|
|
958
|
+
cmd += `\n`
|
|
959
|
+
}
|
|
960
|
+
cmd += `})\n.then(res => {\n const contentType = res.headers.get("content-type");\n if (contentType && contentType.indexOf("application/json") !== -1) {\n return res.json();\n } else {\n return res.text();\n }\n})\n.then(data => console.log("Response:", data))\n.catch(err => console.error("Error:", err));`
|
|
961
|
+
|
|
962
|
+
navigator.clipboard.writeText(cmd).then(() => {
|
|
963
|
+
const btn = document.getElementById('btnCopyFetch')
|
|
964
|
+
btn.innerText = '✓ Copied!'
|
|
965
|
+
setTimeout(() => (btn.innerText = 'Copy fetch'), 2000)
|
|
966
|
+
})
|
|
967
|
+
})
|
|
968
|
+
|
|
969
|
+
// Prod URL HTTP Client logic
|
|
970
|
+
document.getElementById('btnSendProd').addEventListener('click', async () => {
|
|
971
|
+
const url = document.getElementById('prodUrlInput').value.trim()
|
|
972
|
+
if (!url) return alert('Please enter a Production URL first.')
|
|
973
|
+
const ep = getActiveEndpointData()
|
|
974
|
+
|
|
975
|
+
document.getElementById('prodResponseWrapper').style.display = 'block'
|
|
976
|
+
editors.prodResp.setValue('Sending request...', -1)
|
|
977
|
+
document.getElementById('prodStatus').innerText = 'Pending'
|
|
978
|
+
document.getElementById('prodTime').innerText = '--'
|
|
979
|
+
|
|
980
|
+
try {
|
|
981
|
+
const res = await fetch('/api/proxy', {
|
|
982
|
+
method: 'POST',
|
|
983
|
+
headers: { 'Content-Type': 'application/json' },
|
|
984
|
+
body: JSON.stringify({
|
|
985
|
+
url: url,
|
|
986
|
+
method: ep.method,
|
|
987
|
+
params: editors.params.getValue(),
|
|
988
|
+
payload: editors.payload.getValue(),
|
|
989
|
+
}),
|
|
990
|
+
})
|
|
991
|
+
const data = await res.json()
|
|
992
|
+
|
|
993
|
+
if (data.error && !data.status) {
|
|
994
|
+
document.getElementById('prodStatus').innerText = 'ERROR'
|
|
995
|
+
editors.prodResp.setValue(JSON.stringify(data.error, null, 2), -1)
|
|
996
|
+
} else {
|
|
997
|
+
document.getElementById('prodStatus').innerText = data.status + ' Http St'
|
|
998
|
+
document.getElementById('prodTime').innerText = data.time + 'ms'
|
|
999
|
+
let outputStr =
|
|
1000
|
+
typeof data.body === 'object' ? JSON.stringify(data.body, null, 2) : data.body
|
|
1001
|
+
editors.prodResp.setValue(outputStr, -1)
|
|
1002
|
+
}
|
|
1003
|
+
} catch (err) {
|
|
1004
|
+
document.getElementById('prodStatus').innerText = 'CRASH'
|
|
1005
|
+
editors.prodResp.setValue('Network or proxy engine error.', -1)
|
|
1006
|
+
}
|
|
1007
|
+
})
|
|
1008
|
+
}
|