@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.
@@ -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
+ }