@metricinsights/pp-dev 1.5.0-beta.34 → 1.5.0-beta.36

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.
Files changed (35) hide show
  1. package/README.md +17 -9
  2. package/dist/cjs/auth.provider-D5AI-ujG.js +2 -0
  3. package/dist/cjs/auth.provider-D5AI-ujG.js.map +1 -0
  4. package/dist/cjs/index.js +1 -1
  5. package/dist/cjs/index.js.map +1 -1
  6. package/dist/cjs/plugin.js +1 -1
  7. package/dist/cjs/plugin.js.map +1 -1
  8. package/dist/cjs/{version-manifest-DdWlzzZ6.js → version-manifest-DuXhzqGr.js} +2 -2
  9. package/dist/cjs/{version-manifest-DdWlzzZ6.js.map → version-manifest-DuXhzqGr.js.map} +1 -1
  10. package/dist/cjs/version-plugin-DrG2-mQf.js +2 -0
  11. package/dist/cjs/{version-plugin-B1Sb07cW.js.map → version-plugin-DrG2-mQf.js.map} +1 -1
  12. package/dist/client/client.js +113 -53
  13. package/dist/client/client.js.map +1 -1
  14. package/dist/client/request-inspector.js +12 -1
  15. package/dist/client/request-inspector.js.map +1 -1
  16. package/dist/client/variables-editor.js +169 -36
  17. package/dist/client/variables-editor.js.map +1 -1
  18. package/dist/esm/cli.js +1 -1
  19. package/dist/esm/cli.js.map +1 -1
  20. package/dist/esm/{internal.middleware-CC20He9L.js → internal.middleware-wi0Mb4oZ.js} +2 -2
  21. package/dist/esm/internal.middleware-wi0Mb4oZ.js.map +1 -0
  22. package/dist/esm/plugin.js +1 -1
  23. package/package.json +6 -8
  24. package/dist/cjs/auth.provider-6rZUaR33.js +0 -2
  25. package/dist/cjs/auth.provider-6rZUaR33.js.map +0 -1
  26. package/dist/cjs/cli.js +0 -2
  27. package/dist/cjs/cli.js.map +0 -1
  28. package/dist/cjs/index-Cc-bAhnE.js +0 -2
  29. package/dist/cjs/index-Cc-bAhnE.js.map +0 -1
  30. package/dist/cjs/internal.middleware-BlM7gqkq.js +0 -2
  31. package/dist/cjs/internal.middleware-BlM7gqkq.js.map +0 -1
  32. package/dist/cjs/migrate-DDdeB-Fj.js +0 -2
  33. package/dist/cjs/migrate-DDdeB-Fj.js.map +0 -1
  34. package/dist/cjs/version-plugin-B1Sb07cW.js +0 -2
  35. package/dist/esm/internal.middleware-CC20He9L.js.map +0 -1
@@ -364,6 +364,70 @@ const find = (root, selector) => root.querySelector(selector);
364
364
  // Enter on a focused button is that button's own click (Cancel, ✕, the diff's ←/→), not the
365
365
  // dialog's confirmation: as the latter, it saved or discarded on a Cancel reached by Shift+Tab.
366
366
  const isButton = (target) => target instanceof HTMLButtonElement;
367
+ const FOCUSABLE = 'button:not([disabled]), input:not([disabled]), textarea:not([disabled]), select:not([disabled]), a[href], [tabindex]:not([tabindex="-1"])';
368
+ /**
369
+ * Listens for `overlay`'s keys on the document while it is the dialog on top, and returns the
370
+ * function that stops listening (also stopped once the overlay leaves the page).
371
+ *
372
+ * On the overlay itself, the keys reached a dialog only while the focus was inside it: a click on
373
+ * the diff's text or the dialog's body put the focus on `<body>`, and Escape (or ←/→) did nothing.
374
+ * Tab cycles within the dialog: it left it for the page behind (the toolbar's Save).
375
+ */
376
+ function bindModalKeys(overlay, onKey) {
377
+ const listener = (ev) => {
378
+ if (!overlay.isConnected) {
379
+ document.removeEventListener('keydown', listener);
380
+ return;
381
+ }
382
+ const overlays = document.querySelectorAll('.ve-modal-overlay');
383
+ if (overlays[overlays.length - 1] !== overlay) {
384
+ return;
385
+ }
386
+ if (ev.key === 'Tab') {
387
+ trapTab(overlay, ev);
388
+ return;
389
+ }
390
+ onKey(ev);
391
+ };
392
+ document.addEventListener('keydown', listener);
393
+ return () => document.removeEventListener('keydown', listener);
394
+ }
395
+ /**
396
+ * Whether `el` can take the focus: not hidden, itself or by an element between it and `overlay`
397
+ * (the import picker hides the pages its filter leaves out with `display: none`). A hidden last
398
+ * control let Tab leave the dialog, and Shift+Tab tried to focus it.
399
+ */
400
+ function isTabbable(el, overlay) {
401
+ for (let node = el; node && node !== overlay; node = node.parentElement) {
402
+ if (node.hidden) {
403
+ return false;
404
+ }
405
+ const style = getComputedStyle(node);
406
+ if (style.display === 'none' || style.visibility === 'hidden') {
407
+ return false;
408
+ }
409
+ }
410
+ return true;
411
+ }
412
+ function trapTab(overlay, ev) {
413
+ const focusable = Array.from(overlay.querySelectorAll(FOCUSABLE)).filter((el) => isTabbable(el, overlay));
414
+ if (focusable.length === 0) {
415
+ ev.preventDefault();
416
+ return;
417
+ }
418
+ const first = focusable[0];
419
+ const last = focusable[focusable.length - 1];
420
+ const active = document.activeElement;
421
+ const inside = active instanceof Node && overlay.contains(active);
422
+ if (ev.shiftKey && (!inside || active === first)) {
423
+ ev.preventDefault();
424
+ last.focus();
425
+ }
426
+ else if (!ev.shiftKey && (!inside || active === last)) {
427
+ ev.preventDefault();
428
+ first.focus();
429
+ }
430
+ }
367
431
  // Asks for a value (in place of window.prompt()); `onDone` gets `null` when the user cancels.
368
432
  function showCustomValueModal(title, initialValue, onDone) {
369
433
  const overlay = openOverlay('<div class="ve-modal">' +
@@ -382,6 +446,7 @@ function showCustomValueModal(title, initialValue, onDone) {
382
446
  inputEl.focus();
383
447
  inputEl.select();
384
448
  function close(value) {
449
+ unbindKeys();
385
450
  overlay.remove();
386
451
  onDone(value);
387
452
  }
@@ -392,8 +457,8 @@ function showCustomValueModal(title, initialValue, onDone) {
392
457
  close(null);
393
458
  }
394
459
  });
395
- inputEl.addEventListener('keydown', (ev) => {
396
- if (ev.key === 'Enter') {
460
+ const unbindKeys = bindModalKeys(overlay, (ev) => {
461
+ if (ev.key === 'Enter' && !isButton(ev.target)) {
397
462
  ev.preventDefault();
398
463
  close(inputEl.value);
399
464
  }
@@ -417,6 +482,7 @@ function showConfirmModal(message, onConfirm) {
417
482
  '</div>');
418
483
  find(overlay, '.ve-modal-ok').focus();
419
484
  function close(confirmed) {
485
+ unbindKeys();
420
486
  overlay.remove();
421
487
  if (confirmed) {
422
488
  onConfirm();
@@ -429,7 +495,7 @@ function showConfirmModal(message, onConfirm) {
429
495
  close(false);
430
496
  }
431
497
  });
432
- overlay.addEventListener('keydown', (ev) => {
498
+ const unbindKeys = bindModalKeys(overlay, (ev) => {
433
499
  if (ev.key === 'Enter' && !isButton(ev.target)) {
434
500
  ev.preventDefault();
435
501
  close(true);
@@ -456,6 +522,7 @@ function showInfoModal(title, bodyHtml) {
456
522
  '</div>');
457
523
  find(overlay, '.ve-modal-ok').focus();
458
524
  function close() {
525
+ unbindKeys();
459
526
  overlay.remove();
460
527
  }
461
528
  find(overlay, '.ve-modal-ok').addEventListener('click', close);
@@ -464,8 +531,8 @@ function showInfoModal(title, bodyHtml) {
464
531
  close();
465
532
  }
466
533
  });
467
- overlay.addEventListener('keydown', (ev) => {
468
- if (ev.key === 'Enter' || ev.key === 'Escape') {
534
+ const unbindKeys = bindModalKeys(overlay, (ev) => {
535
+ if ((ev.key === 'Enter' && !isButton(ev.target)) || ev.key === 'Escape') {
469
536
  ev.preventDefault();
470
537
  close();
471
538
  }
@@ -618,6 +685,7 @@ function showJsonDiffModal(beforeText, afterText, onConfirm) {
618
685
  }
619
686
  }
620
687
  function close(confirmed) {
688
+ unbindKeys();
621
689
  overlay.remove();
622
690
  if (confirmed && typeof onConfirm === 'function') {
623
691
  onConfirm();
@@ -631,7 +699,7 @@ function showJsonDiffModal(beforeText, afterText, onConfirm) {
631
699
  close(false);
632
700
  }
633
701
  });
634
- overlay.addEventListener('keydown', (ev) => {
702
+ const unbindKeys = bindModalKeys(overlay, (ev) => {
635
703
  if (ev.key === 'ArrowLeft' && hasChanges) {
636
704
  ev.preventDefault();
637
705
  if (currentChange > 0) {
@@ -1789,6 +1857,9 @@ function startVariablesEditor({ templateLess }) {
1789
1857
  let valuesRawMode = activeTab === 'values' && valuesJsonModeFromUrl();
1790
1858
  let rawMode = false;
1791
1859
  let dirty = false;
1860
+ // A save's PUT and its reload are under way: another Save would diff against the baseline the
1861
+ // first one is about to replace, and send a second full replacement.
1862
+ let saving = false;
1792
1863
  let editSeq = 0;
1793
1864
  let banner = null;
1794
1865
  // Last-known saved JSON for each tab — used to build the post-save GitHub-style diff.
@@ -1854,7 +1925,8 @@ function startVariablesEditor({ templateLess }) {
1854
1925
  if (seq !== schemaSeq) {
1855
1926
  return false;
1856
1927
  }
1857
- showBanner('error', 'Failed to load the schema: ' + escapeHtml(errorMessage(e)));
1928
+ // Edits made meanwhile stay on screen, as they do when the load succeeds.
1929
+ showBanner('error', 'Failed to load the schema: ' + escapeHtml(errorMessage(e)), editSeq !== editSeqAtStart);
1858
1930
  return false;
1859
1931
  }
1860
1932
  if (seq !== schemaSeq) {
@@ -1862,10 +1934,15 @@ function startVariablesEditor({ templateLess }) {
1862
1934
  }
1863
1935
  // An error answer (e.g. 503 with no dist service) is not a schema: kept as one, it rendered as
1864
1936
  // "No local __template_variables.json found" and offered to create a file over the real one.
1937
+ // Unless the tab was edited while the load ran: the edits, and the schema they were made on,
1938
+ // are kept on screen (the failed reload emptied the tab and the typing was gone).
1865
1939
  if (!r.ok) {
1866
- schemaState = null;
1867
- schemaRows = [];
1868
- showBanner('error', 'Failed to load the schema: ' + escapeHtml((data && data.error) || 'HTTP ' + r.status));
1940
+ const edited = editSeq !== editSeqAtStart;
1941
+ if (!edited) {
1942
+ schemaState = null;
1943
+ schemaRows = [];
1944
+ }
1945
+ showBanner('error', 'Failed to load the schema: ' + escapeHtml((data && data.error) || 'HTTP ' + r.status), edited);
1869
1946
  return false;
1870
1947
  }
1871
1948
  const freshRows = data.schema && Array.isArray(data.schema.tags) ? data.schema.tags.map((t) => Object.assign({}, t)) : [];
@@ -1883,6 +1960,19 @@ function startVariablesEditor({ templateLess }) {
1883
1960
  }
1884
1961
  async function loadValues(seq, editSeqAtStart) {
1885
1962
  let r;
1963
+ // A save is a full replacement. Keep the state explicitly unavailable after a failed read so
1964
+ // the user cannot accidentally submit an empty replacement set — unless the tab was edited
1965
+ // while the load ran: the edits, and the values they were made on, are kept on screen (the
1966
+ // failed reload emptied the tab and the typing was gone, the tab still dirty).
1967
+ const failed = (html) => {
1968
+ const edited = editSeq !== editSeqAtStart;
1969
+ if (!edited) {
1970
+ valuesState = null;
1971
+ valueRows = [];
1972
+ }
1973
+ showBanner('error', html, edited);
1974
+ return false;
1975
+ };
1886
1976
  try {
1887
1977
  r = await fetch('/@api/variables/values');
1888
1978
  }
@@ -1890,22 +1980,14 @@ function startVariablesEditor({ templateLess }) {
1890
1980
  if (seq !== valuesSeq) {
1891
1981
  return false;
1892
1982
  }
1893
- valuesState = null;
1894
- valueRows = [];
1895
- showBanner('error', 'Failed to load values: ' + escapeHtml(errorMessage(e)));
1896
- return false;
1983
+ return failed('Failed to load values: ' + escapeHtml(errorMessage(e)));
1897
1984
  }
1898
1985
  if (!r.ok) {
1899
1986
  const err = (await r.json().catch(() => ({})));
1900
1987
  if (seq !== valuesSeq) {
1901
1988
  return false;
1902
1989
  }
1903
- // A save is a full replacement. Keep the state explicitly unavailable after a failed
1904
- // read so the user cannot accidentally submit an empty replacement set.
1905
- valuesState = null;
1906
- valueRows = [];
1907
- showBanner('error', escapeHtml(err.error || 'Failed to load values.'));
1908
- return false;
1990
+ return failed(escapeHtml(err.error || 'Failed to load values.'));
1909
1991
  }
1910
1992
  let data;
1911
1993
  try {
@@ -1915,10 +1997,7 @@ function startVariablesEditor({ templateLess }) {
1915
1997
  if (seq !== valuesSeq) {
1916
1998
  return false;
1917
1999
  }
1918
- valuesState = null;
1919
- valueRows = [];
1920
- showBanner('error', 'Failed to parse the values response: ' + escapeHtml(errorMessage(e)));
1921
- return false;
2000
+ return failed('Failed to parse the values response: ' + escapeHtml(errorMessage(e)));
1922
2001
  }
1923
2002
  if (seq !== valuesSeq) {
1924
2003
  return false;
@@ -2104,11 +2183,30 @@ function startVariablesEditor({ templateLess }) {
2104
2183
  rerenderKeepingAdvancedOpen(i);
2105
2184
  });
2106
2185
  }
2186
+ /**
2187
+ * The error for the first row whose additional_options text is not JSON, or null. Such a row
2188
+ * holds the text as typed, as a string: serialized for raw mode or a save, it became a valid
2189
+ * JSON string, and the rows raw mode makes back are new objects, no longer marked as invalid —
2190
+ * so `{bad` was saved as `"{bad"`.
2191
+ */
2192
+ function invalidAdditionalOptionsError() {
2193
+ const index = schemaRows.findIndex((row) => invalidAdditionalOptionsRows.has(row));
2194
+ if (index === -1) {
2195
+ return null;
2196
+ }
2197
+ const tagName = schemaRows[index].name || 'row ' + (index + 1);
2198
+ return 'additional_options must be valid JSON for "' + escapeHtml(tagName) + '".';
2199
+ }
2107
2200
  function toggleRawMode() {
2108
2201
  if (!schemaState) {
2109
2202
  return;
2110
2203
  }
2111
2204
  if (!rawMode) {
2205
+ const invalid = invalidAdditionalOptionsError();
2206
+ if (invalid) {
2207
+ showBanner('error', invalid + ' Fix it before switching to raw JSON.', true);
2208
+ return;
2209
+ }
2112
2210
  // Entering raw mode: serialize the current table state so edits aren't lost.
2113
2211
  schemaState.raw = JSON.stringify(Object.assign({}, schemaState.schema, { tags: schemaRows }), null, 2);
2114
2212
  }
@@ -2468,10 +2566,9 @@ function startVariablesEditor({ templateLess }) {
2468
2566
  raw = byId('raw-editor').value;
2469
2567
  }
2470
2568
  else {
2471
- const invalidAdditionalOptionsIndex = schemaRows.findIndex((row) => invalidAdditionalOptionsRows.has(row));
2472
- if (invalidAdditionalOptionsIndex !== -1) {
2473
- const tagName = schemaRows[invalidAdditionalOptionsIndex].name || 'row ' + (invalidAdditionalOptionsIndex + 1);
2474
- showBanner('error', 'additional_options must be valid JSON for "' + escapeHtml(tagName) + '".');
2569
+ const invalid = invalidAdditionalOptionsError();
2570
+ if (invalid) {
2571
+ showBanner('error', invalid);
2475
2572
  return;
2476
2573
  }
2477
2574
  raw = JSON.stringify(Object.assign({}, schemaState.schema, { tags: schemaRows }), null, 2);
@@ -2481,7 +2578,21 @@ function startVariablesEditor({ templateLess }) {
2481
2578
  showBanner('success', 'No JSON changes to save.');
2482
2579
  return;
2483
2580
  }
2484
- showJsonDiffModal(beforeText, raw, () => void commitSchemaSave(raw).catch(showSaveError));
2581
+ showJsonDiffModal(beforeText, raw, () => commitSave(() => commitSchemaSave(raw)));
2582
+ }
2583
+ /** Runs a confirmed save, with Save off until it and its reload are done (see `saving`). */
2584
+ function commitSave(commit) {
2585
+ setSaving(true);
2586
+ void commit()
2587
+ .catch(showSaveError)
2588
+ .finally(() => setSaving(false));
2589
+ }
2590
+ function setSaving(value) {
2591
+ saving = value;
2592
+ const saveBtn = byId('save-btn');
2593
+ if (saveBtn) {
2594
+ saveBtn.disabled = value;
2595
+ }
2485
2596
  }
2486
2597
  // Every way a save can fail ends in a visible error: a lost save must never look like a pending
2487
2598
  // one. The banner leaves the tab as it is (preserveContent), so the unsaved edits, raw JSON drafts
@@ -2546,13 +2657,15 @@ function startVariablesEditor({ templateLess }) {
2546
2657
  const message = warnings.length
2547
2658
  ? 'Saved, with warning(s):<ul>' + warnings.map((w) => '<li>' + escapeHtml(w) + '</li>').join('') + '</ul>'
2548
2659
  : 'Schema saved.';
2660
+ // What the file holds now, also with newer edits: the next diff compares with it (it showed
2661
+ // the lines just saved as changes again).
2662
+ jsonBaseline.schema = raw;
2549
2663
  if (hasNewerEdits) {
2550
2664
  showBanner('warning', message + '<div>Newer edits remain unsaved.</div>', true);
2551
2665
  return;
2552
2666
  }
2553
2667
  setDirty(false);
2554
2668
  showBanner(warnings.length ? 'warning' : 'success', message);
2555
- jsonBaseline.schema = raw;
2556
2669
  await reloadAfterSave('schema');
2557
2670
  }
2558
2671
  // ── Values tab ───────────────────────────────────────────────────────────────
@@ -2924,17 +3037,22 @@ function startVariablesEditor({ templateLess }) {
2924
3037
  closeImportModal();
2925
3038
  }
2926
3039
  });
2927
- overlay.addEventListener('keydown', (ev) => {
3040
+ document.body.appendChild(overlay);
3041
+ // On the document, not the overlay: with nothing focused in the dialog (its error state, a
3042
+ // click on its text) Escape reached nothing.
3043
+ unbindImportKeys = bindModalKeys(overlay, (ev) => {
2928
3044
  if (ev.key === 'Escape') {
2929
3045
  ev.preventDefault();
2930
3046
  closeImportModal();
2931
3047
  }
2932
3048
  });
2933
- document.body.appendChild(overlay);
2934
3049
  void renderImportPickerStep(overlay);
2935
3050
  }
3051
+ let unbindImportKeys = null;
2936
3052
  function closeImportModal() {
2937
3053
  const overlay = byId('ve-import-overlay');
3054
+ unbindImportKeys?.();
3055
+ unbindImportKeys = null;
2938
3056
  if (overlay) {
2939
3057
  overlay.remove();
2940
3058
  }
@@ -3121,7 +3239,9 @@ function startVariablesEditor({ templateLess }) {
3121
3239
  showBanner('error', 'Cannot save values until live variables have loaded successfully.');
3122
3240
  return;
3123
3241
  }
3124
- let tags = valueRows;
3242
+ // A copy: the table edits its rows in place, so with the rows themselves, what was typed while
3243
+ // the PUT ran went into the saved baseline (and MI's values) the save records afterwards.
3244
+ let tags = valueRows.map((row) => ({ ...row }));
3125
3245
  const beforeText = jsonBaseline.values;
3126
3246
  if (valuesRawMode) {
3127
3247
  try {
@@ -3130,7 +3250,9 @@ function startVariablesEditor({ templateLess }) {
3130
3250
  tags = fromExportableValueRows(JSON.parse(byId('values-raw-editor').value));
3131
3251
  }
3132
3252
  catch (e) {
3133
- showBanner('error', 'Raw JSON is not valid: ' + escapeHtml(errorMessage(e)));
3253
+ // The draft stays as typed: re-rendered, the textarea was made again from the last loaded
3254
+ // rows, and the whole draft was gone.
3255
+ showBanner('error', 'Raw JSON is not valid: ' + escapeHtml(errorMessage(e)), true);
3134
3256
  return;
3135
3257
  }
3136
3258
  }
@@ -3140,7 +3262,7 @@ function startVariablesEditor({ templateLess }) {
3140
3262
  showBanner('success', 'No JSON changes to save.');
3141
3263
  return;
3142
3264
  }
3143
- showJsonDiffModal(beforeText, afterText, () => void commitValuesSave(tags, afterText).catch(showSaveError));
3265
+ showJsonDiffModal(beforeText, afterText, () => commitSave(() => commitValuesSave(tags, afterText)));
3144
3266
  }
3145
3267
  async function commitValuesSave(tags, afterText) {
3146
3268
  const saveEditSeq = editSeq;
@@ -3170,16 +3292,27 @@ function startVariablesEditor({ templateLess }) {
3170
3292
  }
3171
3293
  }
3172
3294
  renderLocalUpdateButton();
3295
+ // What MI holds now, also with newer edits: the next diff compares with it, and the JSON
3296
+ // issues bar no longer takes the saved values for ones changed since the load.
3297
+ jsonBaseline.values = afterText;
3298
+ if (valuesState) {
3299
+ valuesState.combined = tags.map((t) => ({ name: t.name, value: t.value }));
3300
+ }
3301
+ // The JSON issues bar may have been worked out while the PUT ran, against the old values: the
3302
+ // values just saved stayed flagged as changed until the next keystroke.
3303
+ refreshValuesJsonIssues();
3173
3304
  if (hasNewerEdits) {
3174
3305
  showBanner('warning', message + '<div>Newer edits remain unsaved.</div>', true);
3175
3306
  return;
3176
3307
  }
3177
3308
  setDirty(false);
3178
3309
  showBanner(warnings.length ? 'warning' : 'success', message);
3179
- jsonBaseline.values = afterText;
3180
3310
  await reloadAfterSave('values');
3181
3311
  }
3182
3312
  function save() {
3313
+ if (saving) {
3314
+ return;
3315
+ }
3183
3316
  (activeTab === 'schema' ? saveSchema() : saveValues()).catch(showSaveError);
3184
3317
  }
3185
3318
  // ── Local pages ──────────────────────────────────────────────────────────────