mockaton 10.6.5 → 10.6.6

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/src/Dashboard.js CHANGED
@@ -1,6 +1,7 @@
1
- import { AUTOGENERATED_500_COMMENT, HEADER_FOR_502 } from './ApiConstants.js'
1
+ import { createElement as r, createSvgElement as s, className, restoreFocus, deferred, Defer, Fragment, useRef } from './DashboardDom.js'
2
+ import { AUTO_500_COMMENT, HEADER_FOR_502 } from './ApiConstants.js'
2
3
  import { parseFilename, extractComments } from './Filename.js'
3
- import { Commander } from './ApiCommander.js'
4
+ import { store, dittoSplitPaths } from './DashboardStore.js'
4
5
 
5
6
 
6
7
  const CSS = {
@@ -29,10 +30,8 @@ const CSS = {
29
30
 
30
31
  chosen: null,
31
32
  dittoDir: null,
32
- empty: null,
33
33
  leftSide: null,
34
34
  nonDefault: null,
35
- red: null,
36
35
  rightSide: null,
37
36
  status4xx: null,
38
37
 
@@ -56,171 +55,31 @@ const FocusGroup = {
56
55
  PreviewLink: 3
57
56
  }
58
57
 
59
- const mockaton = new Commander(location.origin, parseError, onError)
60
-
61
- const store = /** @type {State} */ {
62
- brokersByMethod: {},
63
- staticBrokers: {},
64
- cookies: [],
65
- comments: [],
66
- delay: 0,
67
-
68
- collectProxied: false,
69
- proxyFallback: '',
70
- get canProxy() {
71
- return Boolean(store.proxyFallback)
72
- },
73
-
74
- leftSideWidth: window.innerWidth / 2,
75
-
76
- groupByMethod: initPreference('groupByMethod'),
77
- toggleGroupByMethod() {
78
- store.groupByMethod = !store.groupByMethod
79
- togglePreference('groupByMethod', store.groupByMethod)
80
- render()
81
- },
82
-
83
- chosenLink: {
84
- method: '',
85
- urlMask: ''
86
- },
87
- get hasChosenLink() {
88
- return store.chosenLink.method
89
- && store.chosenLink.urlMask
90
- },
91
- setChosenLink(method, urlMask) {
92
- store.chosenLink = { method, urlMask }
93
- },
94
-
95
- reset() {
96
- store.setChosenLink('', '')
97
- mockaton.reset()
98
- .then(fetchState)
99
- },
100
-
101
- bulkSelectByComment(value) {
102
- mockaton.bulkSelectByComment(value)
103
- .then(fetchState)
104
- },
105
-
106
-
107
- setGlobalDelay(value) {
108
- store.delay = value
109
- mockaton.setGlobalDelay(value)
110
- },
111
-
112
- selectCookie(name) {
113
- store.cookies = store.cookies.map(([n]) => [n, n === name])
114
- mockaton.selectCookie(name)
115
- },
116
-
117
- setProxyFallback(value) {
118
- store.proxyFallback = value
119
- mockaton.setProxyFallback(value)
120
- .then(render)
121
- },
122
-
123
- setCollectProxied(checked) {
124
- store.collectProxied = checked
125
- mockaton.setCollectProxied(checked)
126
- },
127
-
128
- brokerFor(method, urlMask) { return store.brokersByMethod[method]?.[urlMask] },
129
- staticBrokerFor(route) { return store.staticBrokers[route] },
130
-
131
- previewLink(method, urlMask) {
132
- store.setChosenLink(method, urlMask)
133
- renderRow(method, urlMask)
134
- },
135
-
136
- selectFile(file) {
137
- mockaton.select(file).then(async response => {
138
- const { method, urlMask } = parseFilename(file)
139
- store.brokerFor(method, urlMask).currentMock = await response.json()
140
- store.setChosenLink(method, urlMask)
141
- renderRow(method, urlMask)
142
- })
143
- },
144
-
145
- toggle500(method, urlMask) {
146
- mockaton.toggle500(method, urlMask).then(async response => {
147
- store.brokerFor(method, urlMask).currentMock = await response.json()
148
- store.setChosenLink(method, urlMask)
149
- renderRow(method, urlMask)
150
- })
151
- },
152
-
153
- toggleProxied(method, urlMask, checked) {
154
- mockaton.setRouteIsProxied(method, urlMask, checked).then(() => {
155
- store.brokerFor(method, urlMask).currentMock.proxied = checked
156
- store.setChosenLink(method, urlMask)
157
- renderRow(method, urlMask)
158
- })
159
- },
160
-
161
- setDelayed(method, urlMask, checked) {
162
- mockaton.setRouteIsDelayed(method, urlMask, checked).then(() => {
163
- store.brokerFor(method, urlMask).currentMock.delayed = checked
164
- })
165
- },
166
-
58
+ store.render = render
59
+ store.renderRow = renderRow
60
+ store.setupPatchCallbacks(parseError, onError)
61
+ store.fetchState().catch(onError)
167
62
 
168
- setDelayedStatic(route, checked) {
169
- store.staticBrokerFor(route).delayed = checked
170
- mockaton.setStaticRouteIsDelayed(route, checked)
171
- },
172
-
173
- setStaticRouteStatus(route, status) {
174
- store.staticBrokerFor(route).status = status
175
- mockaton.setStaticRouteStatus(route, status)
176
- }
177
- }
178
-
179
-
180
- fetchState()
181
63
  initRealTimeUpdates()
182
64
  initKeyboardNavigation()
183
65
 
184
- async function fetchState() {
185
- try {
186
- const response = await mockaton.getState()
187
- if (!response.ok)
188
- throw response.status
189
- Object.assign(store, await response.json())
190
- render()
191
- }
192
- catch (error) {
193
- onError(error)
194
- }
195
- }
196
-
197
66
  function render() {
198
67
  restoreFocus(() => document.body.replaceChildren(...App()))
199
68
  if (store.hasChosenLink)
200
69
  previewMock(store.chosenLink.method, store.chosenLink.urlMask)
201
70
  }
202
71
 
203
- function restoreFocus(cb) {
204
- const focusQuery = selectorFor(document.activeElement)
205
- cb()
206
- if (focusQuery)
207
- document.querySelector(focusQuery)?.focus()
208
- }
209
-
210
- const r = createElement
211
- const s = createSvgElement
212
72
  const t = translation => translation[0]
213
73
 
214
74
  const leftSideRef = useRef()
215
75
 
216
76
  function App() {
217
- const { leftSideWidth } = store
218
77
  return [
219
78
  Header(),
220
79
  r('main', null,
221
80
  r('div', {
222
81
  ref: leftSideRef,
223
- style: { width: leftSideWidth + 'px' },
82
+ style: { width: store.leftSideWidth + 'px' },
224
83
  className: CSS.leftSide
225
84
  },
226
85
  r('table', null,
@@ -232,7 +91,6 @@ function App() {
232
91
  ]
233
92
  }
234
93
 
235
-
236
94
  function Header() {
237
95
  return (
238
96
  r('header', null,
@@ -302,7 +160,7 @@ function BulkSelector() {
302
160
  r('hr'),
303
161
  comments.map(value => r('option', { value }, value)),
304
162
  r('hr'),
305
- r('option', { value: AUTOGENERATED_500_COMMENT }, t`Auto500`)
163
+ r('option', { value: AUTO_500_COMMENT }, t`Auto500`)
306
164
  )))
307
165
  }
308
166
 
@@ -322,11 +180,11 @@ function CookieSelector() {
322
180
  r('option', { value, selected }, value)))))
323
181
  }
324
182
 
183
+
325
184
  function ProxyFallbackField() {
326
185
  const checkboxRef = useRef()
327
186
  function onChange() {
328
187
  checkboxRef.current.disabled = !this.validity.valid || !this.value.trim()
329
-
330
188
  if (!this.validity.valid)
331
189
  this.reportValidity()
332
190
  else
@@ -359,6 +217,7 @@ function SaveProxiedCheckbox(ref) {
359
217
  r('span', null, t`Save Mocks`)))
360
218
  }
361
219
 
220
+
362
221
  function ResetButton() {
363
222
  return (
364
223
  r('button', {
@@ -367,6 +226,7 @@ function ResetButton() {
367
226
  }, t`Reset`))
368
227
  }
369
228
 
229
+
370
230
  function SettingsMenuTrigger() {
371
231
  const id = '_settings_menu_'
372
232
  return (
@@ -381,16 +241,15 @@ function SettingsMenuTrigger() {
381
241
 
382
242
  function SettingsMenu(id) {
383
243
  const firstInputRef = useRef()
384
- function onToggle(event) {
385
- if (event.newState === 'open')
386
- firstInputRef.current.focus()
387
- }
388
244
  return (
389
245
  r('menu', {
390
246
  id,
391
247
  popover: '',
392
248
  className: CSS.SettingsMenu,
393
- onToggle
249
+ onToggle(event) {
250
+ if (event.newState === 'open')
251
+ firstInputRef.current.focus()
252
+ }
394
253
  },
395
254
 
396
255
  r('label', className(CSS.GroupByMethod),
@@ -415,9 +274,7 @@ function SettingsMenu(id) {
415
274
 
416
275
  function MockList() {
417
276
  if (!Object.keys(store.brokersByMethod).length)
418
- return (
419
- r('div', className(CSS.empty),
420
- t`No mocks found`))
277
+ return r('div', null, t`No mocks found`)
421
278
 
422
279
  if (store.groupByMethod)
423
280
  return Object.keys(store.brokersByMethod).map(method => Fragment(
@@ -430,13 +287,7 @@ function MockList() {
430
287
  }
431
288
 
432
289
  function rowsFor(targetMethod) {
433
- const rows = []
434
- for (const [method, brokers] of Object.entries(store.brokersByMethod))
435
- if (targetMethod === '*' || targetMethod === method)
436
- for (const [urlMask, broker] of Object.entries(brokers))
437
- rows.push({ method, urlMask, broker })
438
-
439
- const sorted = rows.sort((a, b) => a.urlMask.localeCompare(b.urlMask))
290
+ const sorted = store.brokersByMethodAsArray(targetMethod)
440
291
  const urlMasksDittoed = dittoSplitPaths(sorted.map(r => r.urlMask))
441
292
  return sorted.map((r, i) => ({
442
293
  ...r,
@@ -447,14 +298,26 @@ function rowsFor(targetMethod) {
447
298
  function Row({ method, urlMask, urlMaskDittoed, broker }, i) {
448
299
  const key = Row.key(method, urlMask)
449
300
  Row.ditto.set(key, urlMaskDittoed)
301
+ const { proxied, delayed, file } = broker.currentMock
450
302
  return (
451
303
  r('tr', { key },
452
- store.canProxy && r('td', null, ProxyToggler(broker)),
453
- r('td', null, DelayRouteToggler(broker)),
454
- r('td', null, InternalServerErrorToggler(broker)),
455
- !store.groupByMethod && r('td', className(CSS.Method), method),
456
- r('td', null, PreviewLink(method, urlMask, urlMaskDittoed, i === 0)),
457
- r('td', null, MockSelector(broker))))
304
+ store.canProxy && r('td', null,
305
+ ProxyToggler(method, urlMask, proxied)),
306
+
307
+ r('td', null,
308
+ DelayRouteToggler(method, urlMask, delayed)),
309
+
310
+ r('td', null,
311
+ InternalServerErrorToggler(method, urlMask, parseFilename(file).status === 500)),
312
+
313
+ !store.groupByMethod && r('td', className(CSS.Method),
314
+ method),
315
+
316
+ r('td', null,
317
+ PreviewLink(method, urlMask, urlMaskDittoed, i === 0)),
318
+
319
+ r('td', null,
320
+ MockSelector(broker))))
458
321
  }
459
322
  Row.key = (method, urlMask) => method + '::' + urlMask
460
323
  Row.ditto = new Map()
@@ -473,10 +336,10 @@ function renderRow(method, urlMask) {
473
336
  })
474
337
 
475
338
  function trFor(key) {
476
- return document.querySelector(`body > main > .${CSS.leftSide} tr[key="${key}"]`)
339
+ return leftSideRef.current.querySelector(`tr[key="${key}"]`)
477
340
  }
478
341
  function unChooseOld() {
479
- return document.querySelector(`body > main > .${CSS.leftSide} tr .${CSS.chosen}`)
342
+ return leftSideRef.current.querySelector(`td > .${CSS.chosen}`)
480
343
  ?.classList.remove(CSS.chosen)
481
344
  }
482
345
  }
@@ -504,30 +367,13 @@ function PreviewLink(method, urlMask, urlMaskDittoed, autofocus) {
504
367
 
505
368
  /** @param {ClientMockBroker} broker */
506
369
  function MockSelector(broker) {
507
- const STR_PROXIED = t`Proxied`
508
-
509
370
  let selected = broker.currentMock.file
510
- const { status } = parseFilename(selected)
511
- const files = broker.mocks.filter(item =>
512
- status === 500 ||
513
- !item.includes(AUTOGENERATED_500_COMMENT))
514
-
515
- if (store.canProxy && broker.currentMock.proxied) {
516
- selected = STR_PROXIED
517
- files.push(selected)
518
- }
371
+ const selectedStatus = parseFilename(selected).status
519
372
 
520
- function nameFor(file) {
521
- if (file === STR_PROXIED)
522
- return STR_PROXIED
523
- const { status, ext } = parseFilename(file)
524
- const comments = extractComments(file)
525
- const isAutogen500 = comments.includes(AUTOGENERATED_500_COMMENT)
526
- return [
527
- isAutogen500 ? '' : status,
528
- ext === 'empty' || ext === 'unknown' ? '' : ext,
529
- isAutogen500 ? t`Auto500` : comments.join(' ')
530
- ].filter(Boolean).join(' ')
373
+ const files = baseOptionsFor(broker.mocks, selectedStatus === 500)
374
+ if (store.canProxy && broker.currentMock.proxied) {
375
+ selected = t`Proxied`
376
+ files.push([selected, selected])
531
377
  }
532
378
 
533
379
  return (
@@ -538,35 +384,83 @@ function MockSelector(broker) {
538
384
  disabled: files.length <= 1,
539
385
  ...className(
540
386
  CSS.MockSelector,
541
- selected !== files[0] && CSS.nonDefault,
542
- status >= 400 && status < 500 && CSS.status4xx)
543
- }, files.map(file => (
387
+ selected !== files[0][0] && CSS.nonDefault,
388
+ selectedStatus >= 400 && selectedStatus < 500 && CSS.status4xx)
389
+ }, files.map(([file, name]) => (
544
390
  r('option', {
545
391
  value: file,
546
392
  selected: file === selected
547
- }, nameFor(file))))))
393
+ }, name)))))
548
394
  }
395
+ function baseOptionsFor(mocks, selectedIs500) {
396
+ return mocks
397
+ .filter(f => selectedIs500 || !f.includes(AUTO_500_COMMENT))
398
+ .map(f => [f, nameFor(f)])
549
399
 
550
- /** @param {ClientMockBroker} broker */
551
- function DelayRouteToggler(broker) {
552
- function commit(checked) {
553
- const { method, urlMask } = parseFilename(broker.mocks[0])
554
- store.setDelayed(method, urlMask, checked)
400
+ function nameFor(file) {
401
+ const { status, ext } = parseFilename(file)
402
+ const comments = extractComments(file)
403
+ const isAutogen500 = comments.includes(AUTO_500_COMMENT)
404
+ return [
405
+ isAutogen500 ? '' : status,
406
+ ext === 'empty' || ext === 'unknown' ? '' : ext,
407
+ isAutogen500 ? t`Auto500` : comments.join(' ')
408
+ ].filter(Boolean).join(' ')
555
409
  }
410
+ }
411
+ baseOptionsFor.test = function () {
412
+ (function ignoresAutoGen500WhenUnselected() {
413
+ const files = baseOptionsFor([
414
+ `api/user${AUTO_500_COMMENT}.GET.500.empty`
415
+ ], false)
416
+ console.assert(files.length === 0)
417
+ }());
418
+
419
+ (function keepsNonAutoGen500WhenUnselected() {
420
+ const files = baseOptionsFor([
421
+ `api/user.GET.500.txt`
422
+ ], false)
423
+ console.assert(files.length === 1)
424
+ console.assert(files[0][1] === t`500 txt`)
425
+ }());
426
+
427
+ (function renamesAutoGenFileToAuto500() {
428
+ const files = baseOptionsFor([
429
+ `api/user${AUTO_500_COMMENT}.GET.500.empty`
430
+ ], true)
431
+ console.assert(files.length === 1)
432
+ console.assert(files[0][1] === t`Auto500`)
433
+ }());
434
+
435
+ (function filenameHasExtensionExceptWhenEmptyOrUnknown() {
436
+ const files = baseOptionsFor([
437
+ `api/user0.GET.200.empty`,
438
+ `api/user1.GET.200.unknown`,
439
+ `api/user2.GET.200.json`,
440
+ `api/user3(another json).GET.200.json`,
441
+ ], true)
442
+ console.assert(deepEqual(files.map(([, n]) => n), [
443
+ // Think about, in cases like this, the only option the user
444
+ // has for discerning empty and unknown is on the Previewer Title
445
+ '200',
446
+ '200',
447
+ '200 json',
448
+ '200 json (another json)',
449
+ ]))
450
+ }())
451
+ }
452
+ deferred(baseOptionsFor.test)
453
+
454
+
455
+ function DelayRouteToggler(method, urlMask, checked) {
556
456
  return ClickDragToggler({
557
- checked: broker.currentMock.delayed,
558
- commit,
457
+ checked,
458
+ commit(checked) { store.setDelayed(method, urlMask, checked) },
559
459
  focusGroup: FocusGroup.DelayToggler
560
460
  })
561
461
  }
562
462
 
563
-
564
- /** @param {ClientMockBroker} broker */
565
- function InternalServerErrorToggler(broker) {
566
- function onChange() {
567
- const { method, urlMask } = parseFilename(broker.mocks[0])
568
- store.toggle500(method, urlMask)
569
- }
463
+ function InternalServerErrorToggler(method, urlMask, checked) {
570
464
  return (
571
465
  r('label', {
572
466
  className: CSS.InternalServerErrorToggler,
@@ -574,19 +468,14 @@ function InternalServerErrorToggler(broker) {
574
468
  },
575
469
  r('input', {
576
470
  type: 'checkbox',
577
- 'data-focus-group': FocusGroup.StatusToggler,
578
- checked: parseFilename(broker.currentMock.file).status === 500,
579
- onChange
471
+ checked,
472
+ onChange() { store.toggle500(method, urlMask) },
473
+ 'data-focus-group': FocusGroup.StatusToggler
580
474
  }),
581
475
  r('span', null, t`500`)))
582
476
  }
583
477
 
584
- /** @param {ClientMockBroker} broker */
585
- function ProxyToggler(broker) {
586
- function onChange() {
587
- const { urlMask, method } = parseFilename(broker.mocks[0])
588
- store.toggleProxied(method, urlMask, this.checked)
589
- }
478
+ function ProxyToggler(method, urlMask, checked) {
590
479
  return (
591
480
  r('label', {
592
481
  className: CSS.ProxyToggler,
@@ -594,8 +483,8 @@ function ProxyToggler(broker) {
594
483
  },
595
484
  r('input', {
596
485
  type: 'checkbox',
597
- checked: broker.currentMock.proxied,
598
- onChange,
486
+ checked,
487
+ onChange() { store.setProxied(method, urlMask, this.checked) },
599
488
  'data-focus-group': FocusGroup.ProxyToggler
600
489
  }),
601
490
  CloudIcon()))
@@ -617,34 +506,39 @@ function StaticFilesList() {
617
506
  r('tr', null,
618
507
  r('th', { colspan: (2 + Number(!groupByMethod)) + Number(canProxy) }),
619
508
  r('th', null, t`Static GET`)),
620
- Object.values(staticBrokers).map((broker, i) =>
509
+ Object.values(staticBrokers).map(({ route, status, delayed }, i) =>
621
510
  r('tr', null,
622
511
  canProxy && r('td'),
623
- r('td', null, DelayStaticRouteToggler(broker)),
624
- r('td', null, NotFoundToggler(broker)),
625
- !groupByMethod && r('td', className(CSS.Method), 'GET'),
626
- r('td', null, r('a', {
627
- href: broker.route,
628
- target: '_blank',
629
- className: CSS.PreviewLink,
630
- 'data-focus-group': FocusGroup.PreviewLink
631
- }, dp[i]))
512
+ r('td', null,
513
+ DelayStaticRouteToggler(route, delayed)),
514
+
515
+ r('td', null,
516
+ NotFoundToggler(route, status === 404)),
517
+
518
+ !groupByMethod && r('td', className(CSS.Method),
519
+ 'GET'),
520
+
521
+ r('td', null,
522
+ r('a', {
523
+ href: route,
524
+ target: '_blank',
525
+ className: CSS.PreviewLink,
526
+ 'data-focus-group': FocusGroup.PreviewLink
527
+ }, dp[i]))
632
528
  ))))
633
529
  }
634
530
 
635
- /** @param {ClientStaticBroker} broker */
636
- function DelayStaticRouteToggler(broker) {
531
+ function DelayStaticRouteToggler(route, checked) {
637
532
  return ClickDragToggler({
638
- checked: broker.delayed,
533
+ checked,
639
534
  focusGroup: FocusGroup.DelayToggler,
640
535
  commit(checked) {
641
- store.setDelayedStatic(broker.route, checked)
536
+ store.setDelayedStatic(route, checked)
642
537
  }
643
538
  })
644
539
  }
645
540
 
646
- /** @param {ClientStaticBroker} broker */
647
- function NotFoundToggler(broker) {
541
+ function NotFoundToggler(route, checked) {
648
542
  return (
649
543
  r('label', {
650
544
  className: CSS.NotFoundToggler,
@@ -652,10 +546,10 @@ function NotFoundToggler(broker) {
652
546
  },
653
547
  r('input', {
654
548
  type: 'checkbox',
655
- checked: broker.status === 404,
549
+ checked,
656
550
  'data-focus-group': FocusGroup.StatusToggler,
657
551
  onChange() {
658
- store.setStaticRouteStatus(broker.route, this.checked ? 404 : 200)
552
+ store.setStaticRouteStatus(route, this.checked ? 404 : 200)
659
553
  }
660
554
  }),
661
555
  r('span', null, t`404`)))
@@ -770,7 +664,7 @@ function PayloadViewerTitleWhenProxied({ mime, status, statusText, gatewayIsBad
770
664
  return (
771
665
  r('span', null,
772
666
  gatewayIsBad
773
- ? r('span', className(CSS.red), t`⛔ Fallback Backend Error` + ' ')
667
+ ? r('span', null, t`⛔ Fallback Backend Error` + ' ')
774
668
  : r('span', null, t`Got` + ' '),
775
669
  r('abbr', { title: statusText }, status),
776
670
  ' ' + mime))
@@ -799,11 +693,8 @@ async function previewMock(method, urlMask) {
799
693
  })
800
694
  clearTimeout(spinnerTimer)
801
695
  const { proxied, file } = store.brokerFor(method, urlMask).currentMock
802
- if (proxied)
803
- await updatePayloadViewer(true, '', response)
804
- else if (file)
805
- await updatePayloadViewer(false, file, response)
806
- else {/* e.g. selected was deleted */}
696
+ if (proxied || file)
697
+ await updatePayloadViewer(proxied, file, response)
807
698
  }
808
699
  catch (err) {
809
700
  onError(err)
@@ -826,23 +717,23 @@ async function updatePayloadViewer(proxied, file, response) {
826
717
  file,
827
718
  statusText: response.statusText
828
719
  }))
829
-
720
+
830
721
  if (mime.startsWith('image/')) // Naively assumes GET.200
831
722
  payloadViewerCodeRef.current.replaceChildren(
832
723
  r('img', { src: URL.createObjectURL(await response.blob()) }))
833
724
  else {
834
725
  const body = await response.text() || t`/* Empty Response Body */`
835
726
  if (mime === 'application/json')
836
- payloadViewerCodeRef.current.replaceChildren(r('span', className(CSS.json), syntaxJSON(body)))
837
- else if (isXML(mime))
838
- payloadViewerCodeRef.current.replaceChildren(syntaxXML(body))
727
+ payloadViewerCodeRef.current.replaceChildren(r('span', className(CSS.json), SyntaxJSON(body)))
728
+ else if (isXML(mime))
729
+ payloadViewerCodeRef.current.replaceChildren(SyntaxXML(body))
839
730
  else
840
731
  payloadViewerCodeRef.current.textContent = body
841
732
  }
842
733
  }
843
734
 
844
735
  function isXML(mime) {
845
- return ['text/html', 'text/xml', 'application/xml'].includes(mime)
736
+ return ['text/html', 'text/xml', 'application/xml'].some(m => mime.includes(m))
846
737
  || /application\/.*\+xml/.test(mime)
847
738
  }
848
739
 
@@ -901,6 +792,7 @@ function SettingsIcon() {
901
792
  s('path', { d: 'M19.14 12.94c.04-.3.06-.61.06-.94 0-.32-.02-.64-.07-.94l2.03-1.58c.18-.14.23-.41.12-.61l-1.92-3.32c-.12-.22-.37-.29-.59-.22l-2.39.96c-.5-.38-1.03-.7-1.62-.94l-.36-2.54c-.04-.24-.24-.41-.48-.41h-3.84c-.24 0-.43.17-.47.41l-.36 2.54c-.59.24-1.13.57-1.62.94l-2.39-.96c-.22-.08-.47 0-.59.22L2.74 8.87c-.12.21-.08.47.12.61l2.03 1.58c-.05.3-.09.63-.09.94s.02.64.07.94l-2.03 1.58c-.18.14-.23.41-.12.61l1.92 3.32c.12.22.37.29.59.22l2.39-.96c.5.38 1.03.7 1.62.94l.36 2.54c.05.24.24.41.48.41h3.84c.24 0 .44-.17.47-.41l.36-2.54c.59-.24 1.13-.56 1.62-.94l2.39.96c.22.08.47 0 .59-.22l1.92-3.32c.12-.22.07-.47-.12-.61zM12 15.6c-1.98 0-3.6-1.62-3.6-3.6s1.62-3.6 3.6-3.6 3.6 1.62 3.6 3.6-1.62 3.6-3.6 3.6' })))
902
793
  }
903
794
 
795
+
904
796
  /**
905
797
  * # Long polls UI sync version
906
798
  * The version increments when a mock file is added, removed, or renamed.
@@ -921,14 +813,14 @@ function initRealTimeUpdates() {
921
813
 
922
814
  async function poll() {
923
815
  try {
924
- const response = await mockaton.getSyncVersion(oldSyncVersion, controller.signal)
816
+ const response = await store.getSyncVersion(oldSyncVersion, controller.signal)
925
817
  if (response.ok) {
926
818
  const syncVersion = await response.json()
927
819
  const skipUpdate = oldSyncVersion === -1
928
820
  if (oldSyncVersion !== syncVersion) { // because it could be < or >
929
821
  oldSyncVersion = syncVersion
930
822
  if (!skipUpdate)
931
- await fetchState()
823
+ store.fetchState().catch(onError)
932
824
  }
933
825
  poll()
934
826
  }
@@ -942,6 +834,7 @@ function initRealTimeUpdates() {
942
834
  }
943
835
  }
944
836
 
837
+
945
838
  function initKeyboardNavigation() {
946
839
  addEventListener('keydown', onKeyDown)
947
840
 
@@ -977,8 +870,8 @@ function initKeyboardNavigation() {
977
870
  }
978
871
 
979
872
  function allInFocusGroup(focusGroup) {
980
- return Array.from(document.querySelectorAll(
981
- `body > main > .${CSS.leftSide} table > tr > td [data-focus-group="${focusGroup}"]:is(input, a)`))
873
+ return Array.from(leftSideRef.current.querySelectorAll(
874
+ `tr > td [data-focus-group="${focusGroup}"]:is(input, a)`))
982
875
  }
983
876
 
984
877
  function circularAdjacent(step = 1, arr, pivot) {
@@ -987,172 +880,7 @@ function initKeyboardNavigation() {
987
880
  }
988
881
 
989
882
 
990
- /** # Utils */
991
-
992
- function className(...args) {
993
- return {
994
- className: args.filter(Boolean).join(' ')
995
- }
996
- }
997
-
998
- function createElement(tag, props, ...children) {
999
- const node = document.createElement(tag)
1000
- for (const [k, v] of Object.entries(props || {}))
1001
- if (k === 'ref') v.current = node
1002
- else if (k === 'style') Object.assign(node.style, v)
1003
- else if (k.startsWith('on')) node.addEventListener(k.slice(2).toLowerCase(), ...[v].flat())
1004
- else if (k in node) node[k] = v
1005
- else node.setAttribute(k, v)
1006
- node.append(...children.flat().filter(Boolean))
1007
- return node
1008
- }
1009
-
1010
- function createSvgElement(tagName, props, ...children) {
1011
- const elem = document.createElementNS('http://www.w3.org/2000/svg', tagName)
1012
- for (const [k, v] of Object.entries(props))
1013
- elem.setAttribute(k, v)
1014
- elem.append(...children.flat().filter(Boolean))
1015
- return elem
1016
- }
1017
-
1018
- function useRef() {
1019
- return { current: null }
1020
- }
1021
-
1022
- function Fragment(...args) {
1023
- const frag = new DocumentFragment()
1024
- for (const arg of args)
1025
- if (Array.isArray(arg))
1026
- frag.append(...arg)
1027
- else
1028
- frag.appendChild(arg)
1029
- return frag
1030
- }
1031
-
1032
- function Defer(cb) {
1033
- const placeholder = document.createComment('')
1034
- deferred(() => placeholder.replaceWith(cb()))
1035
- return placeholder
1036
- }
1037
-
1038
- function deferred(cb) {
1039
- return window.requestIdleCallback
1040
- ? requestIdleCallback(cb)
1041
- : setTimeout(cb, 100) // Safari
1042
- }
1043
-
1044
- function selectorFor(elem) {
1045
- if (!(elem instanceof Element))
1046
- return
1047
-
1048
- const path = []
1049
- while (elem) {
1050
- let qualifier = ''
1051
- if (elem.hasAttribute('key'))
1052
- qualifier = `[key="${elem.getAttribute('key')}"]`
1053
- else {
1054
- let i = 0
1055
- let sib = elem
1056
- while ((sib = sib.previousElementSibling))
1057
- if (sib.tagName === elem.tagName)
1058
- i++
1059
- if (i)
1060
- qualifier = `:nth-of-type(${i + 1})`
1061
- }
1062
- path.push(elem.tagName + qualifier)
1063
- elem = elem.parentElement
1064
- }
1065
- return path.reverse().join('>')
1066
- }
1067
-
1068
-
1069
- // When false, the URL will be updated with param=false
1070
- function initPreference(param) {
1071
- const qs = new URLSearchParams(location.search)
1072
- if (!qs.has(param)) {
1073
- const group = localStorage.getItem(param) !== 'false'
1074
- if (!group) {
1075
- const url = new URL(location.href)
1076
- url.searchParams.set(param, false)
1077
- history.replaceState(null, '', url)
1078
- }
1079
- return group
1080
- }
1081
- return qs.get(param) !== 'false'
1082
- }
1083
-
1084
- // When false, the URL and localStorage will have param=false
1085
- function togglePreference(param, nextVal) {
1086
- if (nextVal)
1087
- localStorage.removeItem(param)
1088
- else
1089
- localStorage.setItem(param, nextVal)
1090
-
1091
- const url = new URL(location.href)
1092
- if (nextVal)
1093
- url.searchParams.delete(param)
1094
- else
1095
- url.searchParams.set(param, false)
1096
- history.replaceState(null, '', url)
1097
- }
1098
-
1099
-
1100
- /**
1101
- * Think of this as a way of printing a directory tree in which
1102
- * the repeated folder paths are kept but styled differently.
1103
- * @param {string[]} paths - sorted
1104
- */
1105
- function dittoSplitPaths(paths) {
1106
- const result = [['', paths[0]]]
1107
- const pathsInParts = paths.map(p => p.split('/').filter(Boolean))
1108
-
1109
- for (let i = 1; i < paths.length; i++) {
1110
- const prevParts = pathsInParts[i - 1]
1111
- const currParts = pathsInParts[i]
1112
-
1113
- let j = 0
1114
- while (
1115
- j < currParts.length &&
1116
- j < prevParts.length &&
1117
- currParts[j] === prevParts[j])
1118
- j++
1119
-
1120
- if (!j) // no common dirs
1121
- result.push(['', paths[i]])
1122
- else {
1123
- const ditto = '/' + currParts.slice(0, j).join('/') + '/'
1124
- result.push([ditto, paths[i].slice(ditto.length)])
1125
- }
1126
- }
1127
- return result
1128
- }
1129
-
1130
- dittoSplitPaths.test = function () {
1131
- const input = [
1132
- '/api/user',
1133
- '/api/user/avatar',
1134
- '/api/user/friends',
1135
- '/api/vid',
1136
- '/api/video/id',
1137
- '/api/video/stats',
1138
- '/v2/foo',
1139
- '/v2/foo/bar'
1140
- ]
1141
- const expected = [
1142
- ['', '/api/user'],
1143
- ['/api/user/', 'avatar'],
1144
- ['/api/user/', 'friends'],
1145
- ['/api/', 'vid'],
1146
- ['/api/', 'video/id'],
1147
- ['/api/video/', 'stats'],
1148
- ['', '/v2/foo'],
1149
- ['/v2/foo/', 'bar']
1150
- ]
1151
- console.assert(JSON.stringify(dittoSplitPaths(input)) === JSON.stringify(expected))
1152
- }
1153
-
1154
-
1155
- function syntaxJSON(json) {
883
+ function SyntaxJSON(json) {
1156
884
  const MAX_NODES = 50_000
1157
885
  let nNodes = 0
1158
886
  const frag = new DocumentFragment()
@@ -1172,8 +900,8 @@ function syntaxJSON(json) {
1172
900
 
1173
901
  let match
1174
902
  let lastIndex = 0
1175
- syntaxJSON.regex.lastIndex = 0 // resets regex
1176
- while ((match = syntaxJSON.regex.exec(json)) !== null) {
903
+ SyntaxJSON.regex.lastIndex = 0 // resets regex
904
+ while ((match = SyntaxJSON.regex.exec(json)) !== null) {
1177
905
  if (nNodes > MAX_NODES)
1178
906
  break
1179
907
 
@@ -1195,11 +923,11 @@ function syntaxJSON(json) {
1195
923
  text(json.slice(lastIndex))
1196
924
  return frag
1197
925
  }
1198
- syntaxJSON.regex = /("(?:\\u[a-fA-F0-9]{4}|\\[^u]|[^\\"])*")(\s*:)?|([{}\[\],:\s]+)|\S+/g
926
+ SyntaxJSON.regex = /("(?:\\u[a-fA-F0-9]{4}|\\[^u]|[^\\"])*")(\s*:)?|([{}\[\],:\s]+)|\S+/g
1199
927
  // Capture group order: [string, optional colon, punc]
1200
928
 
1201
929
 
1202
- function syntaxXML(xml) {
930
+ function SyntaxXML(xml) {
1203
931
  const MAX_NODES = 50_000
1204
932
  let nNodes = 0
1205
933
  const frag = new DocumentFragment()
@@ -1219,8 +947,8 @@ function syntaxXML(xml) {
1219
947
 
1220
948
  let match
1221
949
  let lastIndex = 0
1222
- syntaxXML.regex.lastIndex = 0
1223
- while ((match = syntaxXML.regex.exec(xml)) !== null) {
950
+ SyntaxXML.regex.lastIndex = 0
951
+ while ((match = SyntaxXML.regex.exec(xml)) !== null) {
1224
952
  if (nNodes > MAX_NODES)
1225
953
  break
1226
954
 
@@ -1238,6 +966,11 @@ function syntaxXML(xml) {
1238
966
  frag.normalize()
1239
967
  return frag
1240
968
  }
1241
- syntaxXML.regex = /(<\/?|\/?>|\?>)|(?<=<\??\/?)([A-Za-z_:][\w:.-]*)|([A-Za-z_:][\w:.-]*)(?==)|("(?:[^"\\]|\\.)*")/g
969
+ SyntaxXML.regex = /(<\/?|\/?>|\?>)|(?<=<\??\/?)([A-Za-z_:][\w:.-]*)|([A-Za-z_:][\w:.-]*)(?==)|("(?:[^"\\]|\\.)*")/g
1242
970
  // Capture groups order: [tagPunc, tagName, attrName, attrVal]
1243
971
 
972
+
973
+ function deepEqual(a, b) {
974
+ return JSON.stringify(a) === JSON.stringify(b)
975
+ }
976
+