pandorga 1.4.5 → 1.4.7

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.
data/studio/index.html CHANGED
@@ -18,7 +18,7 @@
18
18
  rel="stylesheet"
19
19
  href="https://cdn.jsdelivr.net/gh/lipis/flag-icons@7.2.3/css/flag-icons.min.css"
20
20
  />
21
- <script type="module" crossorigin src="/studio/assets/index-OAmrbW7i.js"></script>
21
+ <script type="module" crossorigin src="/studio/assets/index-BJyN-kB3.js"></script>
22
22
  <link rel="stylesheet" crossorigin href="/studio/assets/index-Cd1mlY_H.css">
23
23
  </head>
24
24
  <body>
@@ -907,9 +907,20 @@ function pipelineChipMeta(pipelineState) {
907
907
  }
908
908
  }
909
909
 
910
+ function pipelineChipViewKey() {
911
+ const meta = pipelineChipMeta(state.pipeline.state);
912
+ return JSON.stringify([
913
+ meta.className,
914
+ meta.spinning,
915
+ meta.icon,
916
+ meta.label,
917
+ meta.title,
918
+ state.pipeline.runUrl || "",
919
+ ]);
920
+ }
921
+
910
922
  function renderPipelineChip() {
911
923
  const meta = pipelineChipMeta(state.pipeline.state);
912
- const runUrl = state.pipeline.runUrl;
913
924
  const kids = [
914
925
  el("span", {
915
926
  className:
@@ -928,11 +939,13 @@ function renderPipelineChip() {
928
939
  "aria-label": meta.label,
929
940
  role: "status",
930
941
  "data-studio-action": "pipeline-status",
931
- disabled: !runUrl,
942
+ "data-pipeline-key": pipelineChipViewKey(),
943
+ disabled: !state.pipeline.runUrl,
932
944
  onClick: () => {
933
- if (!runUrl) return;
945
+ const url = state.pipeline.runUrl;
946
+ if (!url) return;
934
947
  try {
935
- window.open(runUrl, "_blank", "noopener,noreferrer");
948
+ window.open(url, "_blank", "noopener,noreferrer");
936
949
  } catch {
937
950
  /* ignore */
938
951
  }
@@ -942,6 +955,18 @@ function renderPipelineChip() {
942
955
  );
943
956
  }
944
957
 
958
+ /**
959
+ * Swap the toolbar chip only. renderShell() wipes #app and remounts the
960
+ * editor, so a poll tick would steal focus for the whole pipeline run.
961
+ */
962
+ function syncPipelineChip() {
963
+ const current = document.querySelector("[data-studio-action='pipeline-status']");
964
+ if (!current) return;
965
+ const key = pipelineChipViewKey();
966
+ if (current.getAttribute("data-pipeline-key") === key) return;
967
+ current.replaceWith(renderPipelineChip());
968
+ }
969
+
945
970
  function applyPipelinePayload(data) {
946
971
  state.pipeline.runUrl = data.run?.html_url || state.pipeline.runUrl || "";
947
972
  if (data.sha) state.pipeline.sha = String(data.sha).toLowerCase();
@@ -949,12 +974,14 @@ function applyPipelinePayload(data) {
949
974
  if (data.state === "unconfigured") {
950
975
  state.pipeline.state = "unconfigured";
951
976
  state.pipeline.message =
977
+ data.message ||
952
978
  "Set pandorga.content.workflow in _config.yml (e.g. content-pipeline.yml)";
953
979
  return;
954
980
  }
955
981
  state.pipeline.state = data.state || "idle";
956
982
  if (data.state === "error") {
957
- state.pipeline.message = "Content pipeline failed for this commit.";
983
+ state.pipeline.message =
984
+ data.message || "Content pipeline failed for this commit.";
958
985
  }
959
986
  }
960
987
 
@@ -964,14 +991,11 @@ async function refreshPipelineStatus(sha = null) {
964
991
  try {
965
992
  const data = await studioApi.pipelineStatus(sha || null);
966
993
  applyPipelinePayload(data);
967
- renderShell();
994
+ syncPipelineChip();
968
995
  } catch (err) {
969
- const code = err?.code || "";
970
- if (code === "actions_forbidden" || code === "github_unconfigured") {
971
- state.pipeline.state = "error";
972
- state.pipeline.message = String(err.message || err);
973
- renderShell();
974
- }
996
+ state.pipeline.state = "error";
997
+ state.pipeline.message = String(err.message || err);
998
+ syncPipelineChip();
975
999
  }
976
1000
  }
977
1001
 
@@ -986,7 +1010,7 @@ function armPipelineWatch(commitSha) {
986
1010
  state.pipeline.sha = sha;
987
1011
  state.pipeline.runUrl = "";
988
1012
  state.pipeline.message = "";
989
- renderShell();
1013
+ syncPipelineChip();
990
1014
  void pollPipelineStatus(gen, sha);
991
1015
  }
992
1016
 
@@ -1002,7 +1026,7 @@ async function pollPipelineStatus(gen, sha) {
1002
1026
  if (state.pipeline.state === "running" || state.pipeline.state === "awaiting_run") {
1003
1027
  state.pipeline.state = "stalled";
1004
1028
  state.pipeline.message = "Timed out waiting for the content pipeline.";
1005
- renderShell();
1029
+ syncPipelineChip();
1006
1030
  }
1007
1031
  return;
1008
1032
  }
@@ -1014,15 +1038,15 @@ async function pollPipelineStatus(gen, sha) {
1014
1038
  applyPipelinePayload(data);
1015
1039
 
1016
1040
  if (data.state === "unconfigured" || data.state === "ok" || data.state === "error") {
1017
- renderShell();
1041
+ syncPipelineChip();
1018
1042
  return;
1019
1043
  }
1020
1044
  if (data.state === "awaiting_run" && elapsed > STALL_MS) {
1021
1045
  state.pipeline.state = "stalled";
1022
- renderShell();
1046
+ syncPipelineChip();
1023
1047
  return;
1024
1048
  }
1025
- renderShell();
1049
+ syncPipelineChip();
1026
1050
  } catch (err) {
1027
1051
  if (gen !== state.pipeline.pollGen) return;
1028
1052
  softFails += 1;
@@ -1030,13 +1054,13 @@ async function pollPipelineStatus(gen, sha) {
1030
1054
  if (code === "actions_forbidden" || code === "github_unconfigured") {
1031
1055
  state.pipeline.state = "error";
1032
1056
  state.pipeline.message = String(err.message || err);
1033
- renderShell();
1057
+ syncPipelineChip();
1034
1058
  return;
1035
1059
  }
1036
1060
  if (softFails >= 5) {
1037
1061
  state.pipeline.state = "error";
1038
1062
  state.pipeline.message = String(err.message || err);
1039
- renderShell();
1063
+ syncPipelineChip();
1040
1064
  return;
1041
1065
  }
1042
1066
  }
@@ -1118,6 +1142,61 @@ function toolbarActionButton({
1118
1142
  );
1119
1143
  }
1120
1144
 
1145
+ function localExportPresentation() {
1146
+ const exportBusy = state.reexporting || state.pipeline.state === "syncing";
1147
+ return {
1148
+ exportBusy,
1149
+ className:
1150
+ "btn btn-tool btn-reexport" +
1151
+ (exportBusy ? " pipeline-running" : "") +
1152
+ (state.pipeline.state === "ok" && !exportBusy ? " pipeline-ok" : "") +
1153
+ (state.pipeline.state === "error" && !exportBusy ? " pipeline-error" : ""),
1154
+ title: exportBusy
1155
+ ? "Exporting content JSON locally"
1156
+ : "Export content JSON locally and reload",
1157
+ ariaLabel: exportBusy ? "Exporting locally" : "Export locally",
1158
+ icon: exportBusy ? "progress_activity" : "sync",
1159
+ label: exportBusy ? "Exporting…" : "Export",
1160
+ };
1161
+ }
1162
+
1163
+ /** Patch the local Export button without remounting the editor. */
1164
+ function syncLocalExportButton() {
1165
+ const btn = document.querySelector("[data-studio-action='reexport']");
1166
+ if (!btn) return;
1167
+ const exp = localExportPresentation();
1168
+ btn.className = exp.className;
1169
+ btn.title = exp.title;
1170
+ btn.setAttribute("aria-label", exp.ariaLabel);
1171
+ btn.disabled = exp.exportBusy;
1172
+ const icon = btn.querySelector(".material-symbols-outlined");
1173
+ if (icon) {
1174
+ icon.textContent = exp.icon;
1175
+ icon.classList.toggle("studio-icon-spin", exp.exportBusy);
1176
+ }
1177
+ const label = btn.querySelector(".btn-label");
1178
+ if (label) label.textContent = exp.label;
1179
+ }
1180
+
1181
+ function syncToolbarStatus() {
1182
+ const brand = document.querySelector(".studio-toolbar-brand");
1183
+ if (!brand) return;
1184
+ const statusText = state.error ? "Error" : state.status || "";
1185
+ let pill = brand.querySelector(".studio-toolbar-status");
1186
+ if (!statusText) {
1187
+ if (pill) pill.remove();
1188
+ return;
1189
+ }
1190
+ if (!pill) {
1191
+ pill = el("span", { className: "status-pill studio-toolbar-status" });
1192
+ brand.append(pill);
1193
+ }
1194
+ pill.className =
1195
+ "status-pill studio-toolbar-status" + (state.error ? " error" : "");
1196
+ pill.textContent = statusText;
1197
+ pill.title = state.error || state.errorDetail || state.status || "";
1198
+ }
1199
+
1121
1200
  function renderStudioToolbar({ brand = null, menuToggle = null } = {}) {
1122
1201
  const saveLabel = state.saving ? "Saving…" : state.dirty ? "Save" : "Saved";
1123
1202
  const saveIcon = state.saving ? "progress_activity" : state.dirty ? "save" : "check";
@@ -1138,24 +1217,18 @@ function renderStudioToolbar({ brand = null, menuToggle = null } = {}) {
1138
1217
 
1139
1218
  // Theme → pipeline/Export → Revert → Save. Pipeline chip is always present.
1140
1219
  if (isLocalDevHost()) {
1141
- const exportBusy = state.reexporting || state.pipeline.state === "syncing";
1220
+ const exp = localExportPresentation();
1142
1221
  actions.push(
1143
1222
  toolbarActionButton({
1144
- className:
1145
- "btn btn-tool btn-reexport" +
1146
- (exportBusy ? " pipeline-running" : "") +
1147
- (state.pipeline.state === "ok" && !exportBusy ? " pipeline-ok" : "") +
1148
- (state.pipeline.state === "error" && !exportBusy ? " pipeline-error" : ""),
1149
- title: exportBusy
1150
- ? "Exporting content JSON locally"
1151
- : "Export content JSON locally and reload",
1152
- ariaLabel: exportBusy ? "Exporting locally" : "Export locally",
1223
+ className: exp.className,
1224
+ title: exp.title,
1225
+ ariaLabel: exp.ariaLabel,
1153
1226
  action: "reexport",
1154
- disabled: exportBusy,
1227
+ disabled: exp.exportBusy,
1155
1228
  onClick: () => reexportLocalContent(),
1156
- icon: exportBusy ? "progress_activity" : "sync",
1157
- label: exportBusy ? "Exporting…" : "Export",
1158
- spinning: exportBusy,
1229
+ icon: exp.icon,
1230
+ label: exp.label,
1231
+ spinning: exp.exportBusy,
1159
1232
  })
1160
1233
  );
1161
1234
  } else {
@@ -1431,6 +1504,21 @@ function renderErrorBanner() {
1431
1504
  ]);
1432
1505
  }
1433
1506
 
1507
+ /** Show or hide the failure banner without rebuilding the editor. */
1508
+ function syncErrorBanner() {
1509
+ const main = document.querySelector(".studio-main");
1510
+ if (!main) return;
1511
+ const existing = main.querySelector(":scope > .studio-error-banner");
1512
+ const next = renderErrorBanner();
1513
+ if (existing && next) existing.replaceWith(next);
1514
+ else if (existing && !next) existing.remove();
1515
+ else if (!existing && next) {
1516
+ const crumb = main.querySelector(".studio-breadcrumb");
1517
+ if (crumb) crumb.before(next);
1518
+ else main.prepend(next);
1519
+ }
1520
+ }
1521
+
1434
1522
  function renderShell() {
1435
1523
  app.innerHTML = "";
1436
1524
  const groups = collectionGroups(state.schema);
@@ -4009,7 +4097,9 @@ async function reexportLocalContent() {
4009
4097
  state.pipeline.message = "";
4010
4098
  state.status = "Reexporting…";
4011
4099
  clearError();
4012
- renderShell();
4100
+ syncToolbarStatus();
4101
+ syncErrorBanner();
4102
+ syncLocalExportButton();
4013
4103
  try {
4014
4104
  const res = await fetch("/__dev/reexport", {
4015
4105
  method: "POST",
@@ -4024,9 +4114,11 @@ async function reexportLocalContent() {
4024
4114
  if (!res.ok || !data.ok) {
4025
4115
  throw new Error((data && data.error) || `Export failed: ${res.status}`);
4026
4116
  }
4117
+ state.reexporting = false;
4027
4118
  state.pipeline.state = "ok";
4028
4119
  state.status = "Reexported — reloading…";
4029
- renderShell();
4120
+ syncToolbarStatus();
4121
+ syncLocalExportButton();
4030
4122
  setTimeout(() => {
4031
4123
  try {
4032
4124
  const url = new URL(window.location.href);
@@ -4037,12 +4129,13 @@ async function reexportLocalContent() {
4037
4129
  }
4038
4130
  }, 200);
4039
4131
  } catch (err) {
4132
+ state.reexporting = false;
4040
4133
  state.pipeline.state = "error";
4041
4134
  state.pipeline.message = String(err.message || err);
4042
4135
  captureError(err);
4043
- renderShell();
4044
- } finally {
4045
- state.reexporting = false;
4136
+ syncToolbarStatus();
4137
+ syncErrorBanner();
4138
+ syncLocalExportButton();
4046
4139
  }
4047
4140
  }
4048
4141
 
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: pandorga
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.4.5
4
+ version: 1.4.7
5
5
  platform: ruby
6
6
  authors:
7
7
  - The Pandorga contributors
@@ -412,6 +412,8 @@ files:
412
412
  - scripts/test/test-studio-api-contract.rb
413
413
  - scripts/test/test-studio-content-files.rb
414
414
  - scripts/test/test-studio-liquid-roundtrip.mjs
415
+ - scripts/test/test-studio-pipeline-config.mjs
416
+ - scripts/test/test-studio-pipeline-focus.mjs
415
417
  - scripts/test/test-studio-rate-limit.mjs
416
418
  - scripts/test/test-studio-rewrite-api-contract.rb
417
419
  - scripts/test/test-studio-rewrite-sanitize.mjs
@@ -449,8 +451,8 @@ files:
449
451
  - studio-app/src/preview/render.js
450
452
  - studio-app/src/styles/studio.css
451
453
  - studio-app/vite.config.js
454
+ - studio/assets/index-BJyN-kB3.js
452
455
  - studio/assets/index-Cd1mlY_H.css
453
- - studio/assets/index-OAmrbW7i.js
454
456
  - studio/index.html
455
457
  - studio/schema.yml
456
458
  - templates/articles/README.md