@metricinsights/pp-dev 1.5.0-beta.29 → 1.5.0-beta.30

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.
@@ -155,18 +155,23 @@ function importPageGroupHtml(title, pages) {
155
155
  return '';
156
156
  }
157
157
  const rows = pages
158
- .map((p) => '<button type="button" class="ve-import-page-row" data-search="' +
159
- escapeHtml((p.name + ' ' + p.internal_name).toLowerCase()) +
160
- '"' +
161
- actionAttrs('select-import-page', { pageId: p.id, pageName: p.name }) +
162
- '>' +
163
- '<span class="ve-import-page-name">' +
164
- escapeHtml(p.name) +
165
- '</span>' +
166
- '<span class="ve-import-page-slug">' +
167
- escapeHtml(p.internal_name) +
168
- '</span>' +
169
- '</button>')
158
+ .map((p) => {
159
+ // A page without a name on MI goes by its internal name (as an attribute, `null` was
160
+ // dropped, and the review step named the page "undefined").
161
+ const name = p.name || p.internal_name;
162
+ return ('<button type="button" class="ve-import-page-row" data-search="' +
163
+ escapeHtml((name + ' ' + p.internal_name).toLowerCase()) +
164
+ '"' +
165
+ actionAttrs('select-import-page', { pageId: p.id, pageName: name }) +
166
+ '>' +
167
+ '<span class="ve-import-page-name">' +
168
+ escapeHtml(name) +
169
+ '</span>' +
170
+ '<span class="ve-import-page-slug">' +
171
+ escapeHtml(p.internal_name) +
172
+ '</span>' +
173
+ '</button>');
174
+ })
170
175
  .join('');
171
176
  return ('<div class="ve-import-page-group">' +
172
177
  '<div class="ve-import-section-title">' +
@@ -356,6 +361,9 @@ function openOverlay(html) {
356
361
  return overlay;
357
362
  }
358
363
  const find = (root, selector) => root.querySelector(selector);
364
+ // Enter on a focused button is that button's own click (Cancel, ✕, the diff's ←/→), not the
365
+ // dialog's confirmation: as the latter, it saved or discarded on a Cancel reached by Shift+Tab.
366
+ const isButton = (target) => target instanceof HTMLButtonElement;
359
367
  // Asks for a value (in place of window.prompt()); `onDone` gets `null` when the user cancels.
360
368
  function showCustomValueModal(title, initialValue, onDone) {
361
369
  const overlay = openOverlay('<div class="ve-modal">' +
@@ -422,7 +430,7 @@ function showConfirmModal(message, onConfirm) {
422
430
  }
423
431
  });
424
432
  overlay.addEventListener('keydown', (ev) => {
425
- if (ev.key === 'Enter') {
433
+ if (ev.key === 'Enter' && !isButton(ev.target)) {
426
434
  ev.preventDefault();
427
435
  close(true);
428
436
  }
@@ -640,7 +648,7 @@ function showJsonDiffModal(beforeText, afterText, onConfirm) {
640
648
  }
641
649
  return;
642
650
  }
643
- if (ev.key === 'Enter') {
651
+ if (ev.key === 'Enter' && !isButton(ev.target)) {
644
652
  ev.preventDefault();
645
653
  close(true);
646
654
  }
@@ -1692,6 +1700,7 @@ function renderValuesTabHtml(state, rows, rawMode, selectedIndex) {
1692
1700
  <div class="ve-values-layout">
1693
1701
  <div class="ve-values-nav">
1694
1702
  <div class="ve-values-nav-list">${navHtml}</div>
1703
+ <button type="button" class="ve-values-nav-item ve-values-nav-add"${actionAttrs('add-value-row')}>+ Add item</button>
1695
1704
  </div>
1696
1705
  <div class="ve-values-detail">${detailHtml}</div>
1697
1706
  </div>
@@ -1830,7 +1839,11 @@ function startVariablesEditor({ templateLess }) {
1830
1839
  banner = null;
1831
1840
  }
1832
1841
  // ── Data loading ─────────────────────────────────────────────────────────────
1833
- async function loadSchema(seq) {
1842
+ // Both loaders resolve to whether the tab took the data. Edits made while a load ran (the tab
1843
+ // paints what it has first, see scheduleAutoLoad) stay as they are: only what counts as saved
1844
+ // takes the current content, and the caller leaves the tab unpainted, since a repaint would
1845
+ // move the caret.
1846
+ async function loadSchema(seq, editSeqAtStart) {
1834
1847
  let r;
1835
1848
  let data;
1836
1849
  try {
@@ -1839,13 +1852,13 @@ function startVariablesEditor({ templateLess }) {
1839
1852
  }
1840
1853
  catch (e) {
1841
1854
  if (seq !== schemaSeq) {
1842
- return;
1855
+ return false;
1843
1856
  }
1844
1857
  showBanner('error', 'Failed to load the schema: ' + escapeHtml(errorMessage(e)));
1845
- return;
1858
+ return false;
1846
1859
  }
1847
1860
  if (seq !== schemaSeq) {
1848
- return;
1861
+ return false;
1849
1862
  }
1850
1863
  // An error answer (e.g. 503 with no dist service) is not a schema: kept as one, it rendered as
1851
1864
  // "No local __template_variables.json found" and offered to create a file over the real one.
@@ -1853,39 +1866,46 @@ function startVariablesEditor({ templateLess }) {
1853
1866
  schemaState = null;
1854
1867
  schemaRows = [];
1855
1868
  showBanner('error', 'Failed to load the schema: ' + escapeHtml((data && data.error) || 'HTTP ' + r.status));
1856
- return;
1869
+ return false;
1857
1870
  }
1871
+ const freshRows = data.schema && Array.isArray(data.schema.tags) ? data.schema.tags.map((t) => Object.assign({}, t)) : [];
1872
+ const freshRawMode = data.exists && !data.schema;
1873
+ const edited = editSeq !== editSeqAtStart;
1858
1874
  schemaState = data;
1859
- schemaRows =
1860
- data.schema && Array.isArray(data.schema.tags) ? data.schema.tags.map((t) => Object.assign({}, t)) : [];
1861
- rawMode = data.exists && !data.schema;
1862
- jsonBaseline.schema = snapshotSchemaJson();
1875
+ if (!edited) {
1876
+ schemaRows = freshRows;
1877
+ rawMode = freshRawMode;
1878
+ }
1879
+ // In raw mode the file's text, not the textarea's (which may hold edits): the diff shown
1880
+ // before a save compares with it.
1881
+ jsonBaseline.schema = freshRawMode ? data.raw || '' : schemaJsonOf(data, freshRows);
1882
+ return !edited;
1863
1883
  }
1864
- async function loadValues(seq) {
1884
+ async function loadValues(seq, editSeqAtStart) {
1865
1885
  let r;
1866
1886
  try {
1867
1887
  r = await fetch('/@api/variables/values');
1868
1888
  }
1869
1889
  catch (e) {
1870
1890
  if (seq !== valuesSeq) {
1871
- return;
1891
+ return false;
1872
1892
  }
1873
1893
  valuesState = null;
1874
1894
  valueRows = [];
1875
1895
  showBanner('error', 'Failed to load values: ' + escapeHtml(errorMessage(e)));
1876
- return;
1896
+ return false;
1877
1897
  }
1878
1898
  if (!r.ok) {
1879
1899
  const err = (await r.json().catch(() => ({})));
1880
1900
  if (seq !== valuesSeq) {
1881
- return;
1901
+ return false;
1882
1902
  }
1883
1903
  // A save is a full replacement. Keep the state explicitly unavailable after a failed
1884
1904
  // read so the user cannot accidentally submit an empty replacement set.
1885
1905
  valuesState = null;
1886
1906
  valueRows = [];
1887
1907
  showBanner('error', escapeHtml(err.error || 'Failed to load values.'));
1888
- return;
1908
+ return false;
1889
1909
  }
1890
1910
  let data;
1891
1911
  try {
@@ -1893,36 +1913,35 @@ function startVariablesEditor({ templateLess }) {
1893
1913
  }
1894
1914
  catch (e) {
1895
1915
  if (seq !== valuesSeq) {
1896
- return;
1916
+ return false;
1897
1917
  }
1898
1918
  valuesState = null;
1899
1919
  valueRows = [];
1900
1920
  showBanner('error', 'Failed to parse the values response: ' + escapeHtml(errorMessage(e)));
1901
- return;
1921
+ return false;
1902
1922
  }
1903
1923
  if (seq !== valuesSeq) {
1904
- return;
1924
+ return false;
1905
1925
  }
1926
+ const freshRows = (data.combined || []).map((e) => Object.assign({}, e));
1927
+ const edited = editSeq !== editSeqAtStart;
1906
1928
  valuesState = data;
1907
- valueRows = (data.combined || []).map((e) => Object.assign({}, e));
1908
- jsonBaseline.values = snapshotValuesJson();
1909
- }
1910
- function snapshotSchemaJson() {
1911
- if (!schemaState) {
1912
- return '';
1913
- }
1914
- if (rawMode) {
1915
- const editorEl = byId('raw-editor');
1916
- return editorEl ? editorEl.value : schemaState.raw || '';
1929
+ if (!edited) {
1930
+ valueRows = freshRows;
1917
1931
  }
1918
- if (schemaState.raw && (!schemaState.schema || !Array.isArray(schemaRows))) {
1919
- return schemaState.raw;
1932
+ jsonBaseline.values = snapshotValuesJson(freshRows);
1933
+ return !edited;
1934
+ }
1935
+ /** The schema as the file's JSON text would be with `rows` as its tags. */
1936
+ function schemaJsonOf(state, rows) {
1937
+ if (state.raw && !state.schema) {
1938
+ return state.raw;
1920
1939
  }
1921
1940
  try {
1922
- return JSON.stringify(Object.assign({}, schemaState.schema || {}, { tags: schemaRows }), null, 2);
1941
+ return JSON.stringify(Object.assign({}, state.schema || {}, { tags: rows }), null, 2);
1923
1942
  }
1924
1943
  catch {
1925
- return schemaState.raw || '';
1944
+ return state.raw || '';
1926
1945
  }
1927
1946
  }
1928
1947
  function snapshotValuesJson(rows) {
@@ -1936,25 +1955,38 @@ function startVariablesEditor({ templateLess }) {
1936
1955
  // Fetches fresh data for one tab, tracking a per-tab "loading" flag and sequence number so a
1937
1956
  // response that's superseded by a newer load for the SAME tab (started before this one
1938
1957
  // resolved) gets dropped instead of clobbering fresher data. Toggles the tab's own loading
1939
- // flag but deliberately doesn't call render() itself — callers decide when to repaint.
1958
+ // flag but deliberately doesn't call render() itself — callers decide when to repaint, which
1959
+ // they do only when the tab took the data (see loadSchema).
1940
1960
  function loadTabData(tab) {
1961
+ const editSeqAtStart = editSeq;
1941
1962
  if (tab === 'schema') {
1942
1963
  const seq = ++schemaSeq;
1943
1964
  schemaLoading = true;
1944
- return loadSchema(seq).then(() => {
1965
+ return loadSchema(seq, editSeqAtStart).then((taken) => {
1945
1966
  if (seq === schemaSeq) {
1946
1967
  schemaLoading = false;
1947
1968
  }
1969
+ return taken;
1948
1970
  });
1949
1971
  }
1950
1972
  const seq = ++valuesSeq;
1951
1973
  valuesLoading = true;
1952
- return loadValues(seq).then(() => {
1974
+ return loadValues(seq, editSeqAtStart).then((taken) => {
1953
1975
  if (seq === valuesSeq) {
1954
1976
  valuesLoading = false;
1955
1977
  }
1978
+ return taken;
1956
1979
  });
1957
1980
  }
1981
+ /** Repaints the tab once a load is over, or, with edits made meanwhile kept, only the "Refreshing…" dot. */
1982
+ function paintLoaded(taken) {
1983
+ if (taken) {
1984
+ render();
1985
+ }
1986
+ else {
1987
+ updateLoadingIndicator();
1988
+ }
1989
+ }
1958
1990
  // The manual "↻ Refresh" button — deliberately NOT debounced, so it's always the fast path
1959
1991
  // for "I just changed something in MI and want it now" (see scheduleAutoLoad for the
1960
1992
  // debounced, tab-switch-driven path).
@@ -1965,8 +1997,7 @@ function startVariablesEditor({ templateLess }) {
1965
1997
  // render() has to run AFTER that call starts (not before) for the indicator to reflect it.
1966
1998
  const pending = loadTabData(activeTab);
1967
1999
  render();
1968
- await pending;
1969
- render();
2000
+ paintLoaded(await pending);
1970
2001
  }
1971
2002
  function refresh() {
1972
2003
  if (dirty) {
@@ -1990,9 +2021,9 @@ function startVariablesEditor({ templateLess }) {
1990
2021
  if (activeTab === tab) {
1991
2022
  render();
1992
2023
  }
1993
- void pending.then(() => {
2024
+ void pending.then((taken) => {
1994
2025
  if (activeTab === tab) {
1995
- render();
2026
+ paintLoaded(taken);
1996
2027
  }
1997
2028
  });
1998
2029
  }, AUTO_LOAD_DEBOUNCE_MS);
@@ -2488,6 +2519,22 @@ function startVariablesEditor({ templateLess }) {
2488
2519
  showSaveError(new Error(message));
2489
2520
  return null;
2490
2521
  }
2522
+ // The tab's reload after a save, which the save's own banner outlives: a reload that fails is
2523
+ // not a failed save, and says so next to it.
2524
+ async function reloadAfterSave(tab) {
2525
+ const saved = banner;
2526
+ await loadTabData(tab);
2527
+ if (saved && banner !== saved && banner?.type === 'error') {
2528
+ banner = {
2529
+ type: 'warning',
2530
+ html: saved.html +
2531
+ '<div>The save went through, but reloading afterwards did not: ' +
2532
+ banner.html +
2533
+ ' Use ↻ Refresh.</div>',
2534
+ };
2535
+ }
2536
+ render();
2537
+ }
2491
2538
  async function commitSchemaSave(raw) {
2492
2539
  const saveEditSeq = editSeq;
2493
2540
  const data = await putSave('/@api/variables/schema', { raw });
@@ -2506,8 +2553,7 @@ function startVariablesEditor({ templateLess }) {
2506
2553
  setDirty(false);
2507
2554
  showBanner(warnings.length ? 'warning' : 'success', message);
2508
2555
  jsonBaseline.schema = raw;
2509
- await loadTabData('schema');
2510
- render();
2556
+ await reloadAfterSave('schema');
2511
2557
  }
2512
2558
  // ── Values tab ───────────────────────────────────────────────────────────────
2513
2559
  function onComboFocus(i) {
@@ -3045,10 +3091,16 @@ function startVariablesEditor({ templateLess }) {
3045
3091
  showBanner('success', 'Imported ' + applied + ' of ' + total + ' variable(s) from "' + escapeHtml(pageName) + '". Save to apply.');
3046
3092
  }
3047
3093
  function addValueRow() {
3048
- showCustomValueModal('Variable name', '', (name) => {
3094
+ showCustomValueModal('Variable name', '', (entered) => {
3095
+ const name = (entered || '').trim();
3049
3096
  if (!name) {
3050
3097
  return;
3051
3098
  }
3099
+ // MI keeps one value per name: a second row of that name would lose one of the two on save.
3100
+ if (valueRows.some((row) => row.name === name)) {
3101
+ showBanner('error', 'A variable named "' + escapeHtml(name) + '" is in the list already.');
3102
+ return;
3103
+ }
3052
3104
  valueRows.push({ name, value: '' });
3053
3105
  selectedValueIndex = valueRows.length - 1;
3054
3106
  setDirty(true);
@@ -3125,8 +3177,7 @@ function startVariablesEditor({ templateLess }) {
3125
3177
  setDirty(false);
3126
3178
  showBanner(warnings.length ? 'warning' : 'success', message);
3127
3179
  jsonBaseline.values = afterText;
3128
- await loadTabData('values');
3129
- render();
3180
+ await reloadAfterSave('values');
3130
3181
  }
3131
3182
  function save() {
3132
3183
  (activeTab === 'schema' ? saveSchema() : saveValues()).catch(showSaveError);
@@ -3220,14 +3271,18 @@ function startVariablesEditor({ templateLess }) {
3220
3271
  if (activeTab === 'values' && valuesRawMode) {
3221
3272
  refreshValuesJsonIssues();
3222
3273
  }
3274
+ updateLoadingIndicator();
3275
+ }
3276
+ // The "Refreshing…" dot, shown only over cached content already on screen underneath it —
3277
+ // when there is none (state is still null), renderSkeleton() is the loading signal.
3278
+ function updateLoadingIndicator() {
3223
3279
  const loadingEl = byId('loading-indicator');
3224
- if (loadingEl) {
3225
- // Only surface the indicator when there's cached content already on screen underneath it —
3226
- // when there isn't (state is still null), renderSkeleton() above is the loading signal.
3227
- const stateForTab = activeTab === 'schema' ? schemaState : valuesState;
3228
- const isLoading = activeTab === 'schema' ? schemaLoading : valuesLoading;
3229
- loadingEl.style.display = isLoading && stateForTab ? '' : 'none';
3280
+ if (!loadingEl) {
3281
+ return;
3230
3282
  }
3283
+ const stateForTab = activeTab === 'schema' ? schemaState : valuesState;
3284
+ const isLoading = activeTab === 'schema' ? schemaLoading : valuesLoading;
3285
+ loadingEl.style.display = isLoading && stateForTab ? '' : 'none';
3231
3286
  }
3232
3287
  // ── Events ───────────────────────────────────────────────────────────────────
3233
3288
  // One listener per event type for the whole page — the toolbar, the tab's content and the