pandorga 1.4.3 → 1.4.5
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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +24 -0
- data/functions/api/studio/[[path]].js +26 -0
- data/functions/api/studio/_lib/github.js +224 -0
- data/lib/pandorga/version.rb +1 -1
- data/scripts/test/test-studio-api-contract.rb +18 -1
- data/studio/assets/index-Cd1mlY_H.css +1 -0
- data/studio/assets/{index-fGeDVQzi.js → index-OAmrbW7i.js} +100 -100
- data/studio/index.html +2 -2
- data/studio-app/src/api/client.js +8 -0
- data/studio-app/src/main.js +266 -32
- data/studio-app/src/media/browser.js +13 -2
- data/studio-app/src/styles/studio.css +61 -0
- metadata +3 -3
- data/studio/assets/index-B36OvZMr.css +0 -1
data/studio/index.html
CHANGED
|
@@ -18,8 +18,8 @@
|
|
|
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-
|
|
22
|
-
<link rel="stylesheet" crossorigin href="/studio/assets/index-
|
|
21
|
+
<script type="module" crossorigin src="/studio/assets/index-OAmrbW7i.js"></script>
|
|
22
|
+
<link rel="stylesheet" crossorigin href="/studio/assets/index-Cd1mlY_H.css">
|
|
23
23
|
</head>
|
|
24
24
|
<body>
|
|
25
25
|
<div id="app"></div>
|
|
@@ -73,4 +73,12 @@ export const studioApi = {
|
|
|
73
73
|
method: "POST",
|
|
74
74
|
body: JSON.stringify(payload),
|
|
75
75
|
}),
|
|
76
|
+
/** Content-pipeline run for a commit SHA, or latest on the branch when omitted. */
|
|
77
|
+
pipelineStatus: (sha = null, workflow = null) => {
|
|
78
|
+
const params = new URLSearchParams();
|
|
79
|
+
if (sha) params.set("sha", String(sha));
|
|
80
|
+
if (workflow) params.set("workflow", String(workflow));
|
|
81
|
+
const q = params.toString();
|
|
82
|
+
return api(q ? `pipeline-status?${q}` : "pipeline-status");
|
|
83
|
+
},
|
|
76
84
|
};
|
data/studio-app/src/main.js
CHANGED
|
@@ -202,6 +202,19 @@ const state = {
|
|
|
202
202
|
navMenuOpen: false,
|
|
203
203
|
/** Theme menu (light / dark / system) in the icon toolbar. */
|
|
204
204
|
themeMenuOpen: false,
|
|
205
|
+
/**
|
|
206
|
+
* Content publish indicator (local Export animation or production Actions).
|
|
207
|
+
* state: idle | syncing | awaiting_run | running | ok | error | stalled | unconfigured
|
|
208
|
+
*/
|
|
209
|
+
pipeline: {
|
|
210
|
+
state: "idle",
|
|
211
|
+
sha: "",
|
|
212
|
+
runUrl: "",
|
|
213
|
+
message: "",
|
|
214
|
+
pollGen: 0,
|
|
215
|
+
},
|
|
216
|
+
/** Local Export button in flight. */
|
|
217
|
+
reexporting: false,
|
|
205
218
|
/** Per-collection list query (session memory). */
|
|
206
219
|
listQueryByCollection: Object.create(null),
|
|
207
220
|
/** Per-collection sort id (session + localStorage). */
|
|
@@ -432,6 +445,7 @@ function mediaOpts() {
|
|
|
432
445
|
captureError,
|
|
433
446
|
clearError,
|
|
434
447
|
goHome,
|
|
448
|
+
onCommitted: (commitSha) => armPipelineWatch(commitSha),
|
|
435
449
|
};
|
|
436
450
|
}
|
|
437
451
|
|
|
@@ -825,6 +839,212 @@ function isLocalDevHost() {
|
|
|
825
839
|
return host === "localhost" || host === "127.0.0.1" || host === "[::1]";
|
|
826
840
|
}
|
|
827
841
|
|
|
842
|
+
function sleep(ms) {
|
|
843
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
function pipelineChipMeta(pipelineState) {
|
|
847
|
+
switch (pipelineState) {
|
|
848
|
+
case "syncing":
|
|
849
|
+
case "running":
|
|
850
|
+
return {
|
|
851
|
+
label: "Exporting…",
|
|
852
|
+
icon: "progress_activity",
|
|
853
|
+
className: "pipeline-chip pipeline-running",
|
|
854
|
+
spinning: true,
|
|
855
|
+
title: "Content export is running",
|
|
856
|
+
};
|
|
857
|
+
case "awaiting_run":
|
|
858
|
+
return {
|
|
859
|
+
label: "Waiting…",
|
|
860
|
+
icon: "schedule",
|
|
861
|
+
className: "pipeline-chip pipeline-awaiting",
|
|
862
|
+
spinning: false,
|
|
863
|
+
title: "Waiting for the content pipeline run",
|
|
864
|
+
};
|
|
865
|
+
case "ok":
|
|
866
|
+
return {
|
|
867
|
+
label: "Live",
|
|
868
|
+
icon: "check_circle",
|
|
869
|
+
className: "pipeline-chip pipeline-ok",
|
|
870
|
+
spinning: false,
|
|
871
|
+
title: "Content pipeline succeeded for this commit",
|
|
872
|
+
};
|
|
873
|
+
case "error":
|
|
874
|
+
return {
|
|
875
|
+
label: "Failed",
|
|
876
|
+
icon: "error",
|
|
877
|
+
className: "pipeline-chip pipeline-error",
|
|
878
|
+
spinning: false,
|
|
879
|
+
title: state.pipeline.message || "Content pipeline failed",
|
|
880
|
+
};
|
|
881
|
+
case "stalled":
|
|
882
|
+
return {
|
|
883
|
+
label: "No run",
|
|
884
|
+
icon: "hourglass_disabled",
|
|
885
|
+
className: "pipeline-chip pipeline-stalled",
|
|
886
|
+
spinning: false,
|
|
887
|
+
title: "No pipeline run appeared for this commit yet",
|
|
888
|
+
};
|
|
889
|
+
case "unconfigured":
|
|
890
|
+
return {
|
|
891
|
+
label: "Pipeline",
|
|
892
|
+
icon: "cloud_off",
|
|
893
|
+
className: "pipeline-chip pipeline-idle",
|
|
894
|
+
spinning: false,
|
|
895
|
+
title:
|
|
896
|
+
state.pipeline.message ||
|
|
897
|
+
"Set pandorga.content.workflow in _config.yml (e.g. content-pipeline.yml)",
|
|
898
|
+
};
|
|
899
|
+
default:
|
|
900
|
+
return {
|
|
901
|
+
label: "Pipeline",
|
|
902
|
+
icon: "cloud",
|
|
903
|
+
className: "pipeline-chip pipeline-idle",
|
|
904
|
+
spinning: false,
|
|
905
|
+
title: "Content pipeline status",
|
|
906
|
+
};
|
|
907
|
+
}
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
function renderPipelineChip() {
|
|
911
|
+
const meta = pipelineChipMeta(state.pipeline.state);
|
|
912
|
+
const runUrl = state.pipeline.runUrl;
|
|
913
|
+
const kids = [
|
|
914
|
+
el("span", {
|
|
915
|
+
className:
|
|
916
|
+
"material-symbols-outlined" + (meta.spinning ? " studio-icon-spin" : ""),
|
|
917
|
+
text: meta.icon,
|
|
918
|
+
"aria-hidden": "true",
|
|
919
|
+
}),
|
|
920
|
+
el("span", { className: "btn-label", text: meta.label }),
|
|
921
|
+
];
|
|
922
|
+
return el(
|
|
923
|
+
"button",
|
|
924
|
+
{
|
|
925
|
+
className: `btn btn-tool ${meta.className}`,
|
|
926
|
+
type: "button",
|
|
927
|
+
title: meta.title,
|
|
928
|
+
"aria-label": meta.label,
|
|
929
|
+
role: "status",
|
|
930
|
+
"data-studio-action": "pipeline-status",
|
|
931
|
+
disabled: !runUrl,
|
|
932
|
+
onClick: () => {
|
|
933
|
+
if (!runUrl) return;
|
|
934
|
+
try {
|
|
935
|
+
window.open(runUrl, "_blank", "noopener,noreferrer");
|
|
936
|
+
} catch {
|
|
937
|
+
/* ignore */
|
|
938
|
+
}
|
|
939
|
+
},
|
|
940
|
+
},
|
|
941
|
+
kids
|
|
942
|
+
);
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
function applyPipelinePayload(data) {
|
|
946
|
+
state.pipeline.runUrl = data.run?.html_url || state.pipeline.runUrl || "";
|
|
947
|
+
if (data.sha) state.pipeline.sha = String(data.sha).toLowerCase();
|
|
948
|
+
state.pipeline.message = "";
|
|
949
|
+
if (data.state === "unconfigured") {
|
|
950
|
+
state.pipeline.state = "unconfigured";
|
|
951
|
+
state.pipeline.message =
|
|
952
|
+
"Set pandorga.content.workflow in _config.yml (e.g. content-pipeline.yml)";
|
|
953
|
+
return;
|
|
954
|
+
}
|
|
955
|
+
state.pipeline.state = data.state || "idle";
|
|
956
|
+
if (data.state === "error") {
|
|
957
|
+
state.pipeline.message = "Content pipeline failed for this commit.";
|
|
958
|
+
}
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
/** Hydrate the always-visible chip from the latest workflow run (or a SHA). */
|
|
962
|
+
async function refreshPipelineStatus(sha = null) {
|
|
963
|
+
if (isLocalDevHost()) return;
|
|
964
|
+
try {
|
|
965
|
+
const data = await studioApi.pipelineStatus(sha || null);
|
|
966
|
+
applyPipelinePayload(data);
|
|
967
|
+
renderShell();
|
|
968
|
+
} 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
|
+
}
|
|
975
|
+
}
|
|
976
|
+
}
|
|
977
|
+
|
|
978
|
+
/**
|
|
979
|
+
* After a production Save, poll Actions for the content-pipeline run on this SHA.
|
|
980
|
+
*/
|
|
981
|
+
function armPipelineWatch(commitSha) {
|
|
982
|
+
const sha = String(commitSha || "").trim().toLowerCase();
|
|
983
|
+
if (!sha || isLocalDevHost()) return;
|
|
984
|
+
const gen = ++state.pipeline.pollGen;
|
|
985
|
+
state.pipeline.state = "awaiting_run";
|
|
986
|
+
state.pipeline.sha = sha;
|
|
987
|
+
state.pipeline.runUrl = "";
|
|
988
|
+
state.pipeline.message = "";
|
|
989
|
+
renderShell();
|
|
990
|
+
void pollPipelineStatus(gen, sha);
|
|
991
|
+
}
|
|
992
|
+
|
|
993
|
+
async function pollPipelineStatus(gen, sha) {
|
|
994
|
+
const started = Date.now();
|
|
995
|
+
const STALL_MS = 10 * 60 * 1000;
|
|
996
|
+
const MAX_MS = 30 * 60 * 1000;
|
|
997
|
+
let softFails = 0;
|
|
998
|
+
|
|
999
|
+
while (gen === state.pipeline.pollGen) {
|
|
1000
|
+
const elapsed = Date.now() - started;
|
|
1001
|
+
if (elapsed > MAX_MS) {
|
|
1002
|
+
if (state.pipeline.state === "running" || state.pipeline.state === "awaiting_run") {
|
|
1003
|
+
state.pipeline.state = "stalled";
|
|
1004
|
+
state.pipeline.message = "Timed out waiting for the content pipeline.";
|
|
1005
|
+
renderShell();
|
|
1006
|
+
}
|
|
1007
|
+
return;
|
|
1008
|
+
}
|
|
1009
|
+
|
|
1010
|
+
try {
|
|
1011
|
+
const data = await studioApi.pipelineStatus(sha);
|
|
1012
|
+
if (gen !== state.pipeline.pollGen) return;
|
|
1013
|
+
softFails = 0;
|
|
1014
|
+
applyPipelinePayload(data);
|
|
1015
|
+
|
|
1016
|
+
if (data.state === "unconfigured" || data.state === "ok" || data.state === "error") {
|
|
1017
|
+
renderShell();
|
|
1018
|
+
return;
|
|
1019
|
+
}
|
|
1020
|
+
if (data.state === "awaiting_run" && elapsed > STALL_MS) {
|
|
1021
|
+
state.pipeline.state = "stalled";
|
|
1022
|
+
renderShell();
|
|
1023
|
+
return;
|
|
1024
|
+
}
|
|
1025
|
+
renderShell();
|
|
1026
|
+
} catch (err) {
|
|
1027
|
+
if (gen !== state.pipeline.pollGen) return;
|
|
1028
|
+
softFails += 1;
|
|
1029
|
+
const code = err?.code || "";
|
|
1030
|
+
if (code === "actions_forbidden" || code === "github_unconfigured") {
|
|
1031
|
+
state.pipeline.state = "error";
|
|
1032
|
+
state.pipeline.message = String(err.message || err);
|
|
1033
|
+
renderShell();
|
|
1034
|
+
return;
|
|
1035
|
+
}
|
|
1036
|
+
if (softFails >= 5) {
|
|
1037
|
+
state.pipeline.state = "error";
|
|
1038
|
+
state.pipeline.message = String(err.message || err);
|
|
1039
|
+
renderShell();
|
|
1040
|
+
return;
|
|
1041
|
+
}
|
|
1042
|
+
}
|
|
1043
|
+
|
|
1044
|
+
await sleep(elapsed < 60_000 ? 2500 : 7000);
|
|
1045
|
+
}
|
|
1046
|
+
}
|
|
1047
|
+
|
|
828
1048
|
function collectionAllowsCreate(col = state.collection) {
|
|
829
1049
|
if (!col || col.type !== "collection") return false;
|
|
830
1050
|
return col.operations?.create !== false;
|
|
@@ -914,8 +1134,35 @@ function renderStudioToolbar({ brand = null, menuToggle = null } = {}) {
|
|
|
914
1134
|
: null,
|
|
915
1135
|
]);
|
|
916
1136
|
|
|
917
|
-
const actions = [
|
|
918
|
-
|
|
1137
|
+
const actions = [renderThemeMenu()];
|
|
1138
|
+
|
|
1139
|
+
// Theme → pipeline/Export → Revert → Save. Pipeline chip is always present.
|
|
1140
|
+
if (isLocalDevHost()) {
|
|
1141
|
+
const exportBusy = state.reexporting || state.pipeline.state === "syncing";
|
|
1142
|
+
actions.push(
|
|
1143
|
+
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",
|
|
1153
|
+
action: "reexport",
|
|
1154
|
+
disabled: exportBusy,
|
|
1155
|
+
onClick: () => reexportLocalContent(),
|
|
1156
|
+
icon: exportBusy ? "progress_activity" : "sync",
|
|
1157
|
+
label: exportBusy ? "Exporting…" : "Export",
|
|
1158
|
+
spinning: exportBusy,
|
|
1159
|
+
})
|
|
1160
|
+
);
|
|
1161
|
+
} else {
|
|
1162
|
+
actions.push(renderPipelineChip());
|
|
1163
|
+
}
|
|
1164
|
+
|
|
1165
|
+
actions.push(
|
|
919
1166
|
toolbarActionButton({
|
|
920
1167
|
className: "btn btn-tool",
|
|
921
1168
|
title: "Revert all edits to the last loaded or saved version",
|
|
@@ -940,34 +1187,8 @@ function renderStudioToolbar({ brand = null, menuToggle = null } = {}) {
|
|
|
940
1187
|
icon: saveIcon,
|
|
941
1188
|
label: saveLabel,
|
|
942
1189
|
spinning: state.saving,
|
|
943
|
-
})
|
|
944
|
-
|
|
945
|
-
if (state.file && collectionAllowsDelete()) {
|
|
946
|
-
actions.push(
|
|
947
|
-
toolbarActionButton({
|
|
948
|
-
className: "btn btn-tool btn-danger",
|
|
949
|
-
title: "Delete this entry from the repository",
|
|
950
|
-
ariaLabel: "Delete entry",
|
|
951
|
-
action: "delete",
|
|
952
|
-
onClick: () => deleteCurrentEntry(),
|
|
953
|
-
icon: "delete",
|
|
954
|
-
label: "Delete",
|
|
955
|
-
})
|
|
956
|
-
);
|
|
957
|
-
}
|
|
958
|
-
if (isLocalDevHost()) {
|
|
959
|
-
actions.push(
|
|
960
|
-
toolbarActionButton({
|
|
961
|
-
className: "btn btn-tool btn-reexport",
|
|
962
|
-
title: "Export content JSON locally and reload",
|
|
963
|
-
ariaLabel: "Export locally",
|
|
964
|
-
action: "reexport",
|
|
965
|
-
onClick: () => reexportLocalContent(),
|
|
966
|
-
icon: "sync",
|
|
967
|
-
label: "Export",
|
|
968
|
-
})
|
|
969
|
-
);
|
|
970
|
-
}
|
|
1190
|
+
})
|
|
1191
|
+
);
|
|
971
1192
|
|
|
972
1193
|
const kids = [
|
|
973
1194
|
brandCluster,
|
|
@@ -3782,6 +4003,10 @@ function discardChanges() {
|
|
|
3782
4003
|
}
|
|
3783
4004
|
|
|
3784
4005
|
async function reexportLocalContent() {
|
|
4006
|
+
if (state.reexporting) return;
|
|
4007
|
+
state.reexporting = true;
|
|
4008
|
+
state.pipeline.state = "syncing";
|
|
4009
|
+
state.pipeline.message = "";
|
|
3785
4010
|
state.status = "Reexporting…";
|
|
3786
4011
|
clearError();
|
|
3787
4012
|
renderShell();
|
|
@@ -3799,6 +4024,7 @@ async function reexportLocalContent() {
|
|
|
3799
4024
|
if (!res.ok || !data.ok) {
|
|
3800
4025
|
throw new Error((data && data.error) || `Export failed: ${res.status}`);
|
|
3801
4026
|
}
|
|
4027
|
+
state.pipeline.state = "ok";
|
|
3802
4028
|
state.status = "Reexported — reloading…";
|
|
3803
4029
|
renderShell();
|
|
3804
4030
|
setTimeout(() => {
|
|
@@ -3811,8 +4037,12 @@ async function reexportLocalContent() {
|
|
|
3811
4037
|
}
|
|
3812
4038
|
}, 200);
|
|
3813
4039
|
} catch (err) {
|
|
4040
|
+
state.pipeline.state = "error";
|
|
4041
|
+
state.pipeline.message = String(err.message || err);
|
|
3814
4042
|
captureError(err);
|
|
3815
4043
|
renderShell();
|
|
4044
|
+
} finally {
|
|
4045
|
+
state.reexporting = false;
|
|
3816
4046
|
}
|
|
3817
4047
|
}
|
|
3818
4048
|
|
|
@@ -3872,7 +4102,7 @@ async function createNewEntry() {
|
|
|
3872
4102
|
}
|
|
3873
4103
|
if (exists) throw new Error(`${path} already exists`);
|
|
3874
4104
|
|
|
3875
|
-
await studioApi.putFile({
|
|
4105
|
+
const created = await studioApi.putFile({
|
|
3876
4106
|
path,
|
|
3877
4107
|
content,
|
|
3878
4108
|
sha: null,
|
|
@@ -3880,6 +4110,7 @@ async function createNewEntry() {
|
|
|
3880
4110
|
});
|
|
3881
4111
|
invalidateTreeCache(col.name);
|
|
3882
4112
|
state.status = `Created ${path}`;
|
|
4113
|
+
armPipelineWatch(created?.commit);
|
|
3883
4114
|
await selectCollection(col);
|
|
3884
4115
|
await openFile(path);
|
|
3885
4116
|
} catch (err) {
|
|
@@ -3904,7 +4135,7 @@ async function deleteEntry(path, sha = null) {
|
|
|
3904
4135
|
const data = await studioApi.getFile(path);
|
|
3905
4136
|
fileSha = data.sha;
|
|
3906
4137
|
}
|
|
3907
|
-
await studioApi.deleteFile({
|
|
4138
|
+
const deleted = await studioApi.deleteFile({
|
|
3908
4139
|
path,
|
|
3909
4140
|
sha: fileSha,
|
|
3910
4141
|
message: `studio: delete ${path}`,
|
|
@@ -3920,6 +4151,7 @@ async function deleteEntry(path, sha = null) {
|
|
|
3920
4151
|
state.body = "";
|
|
3921
4152
|
}
|
|
3922
4153
|
state.status = `Deleted ${path}`;
|
|
4154
|
+
armPipelineWatch(deleted?.commit);
|
|
3923
4155
|
if (col) await selectCollection(col);
|
|
3924
4156
|
else renderShell();
|
|
3925
4157
|
} catch (err) {
|
|
@@ -3999,6 +4231,7 @@ async function saveCurrent() {
|
|
|
3999
4231
|
captureBaseline();
|
|
4000
4232
|
// List thumbs/titles may have changed — drop session tree for this collection.
|
|
4001
4233
|
if (col?.name) invalidateTreeCache(col.name);
|
|
4234
|
+
armPipelineWatch(result?.commit);
|
|
4002
4235
|
} catch (err) {
|
|
4003
4236
|
captureError(err);
|
|
4004
4237
|
} finally {
|
|
@@ -4034,6 +4267,7 @@ async function bootApp(clerk) {
|
|
|
4034
4267
|
}
|
|
4035
4268
|
|
|
4036
4269
|
renderShell();
|
|
4270
|
+
void refreshPipelineStatus();
|
|
4037
4271
|
|
|
4038
4272
|
scheduleTreePrefetch();
|
|
4039
4273
|
|
|
@@ -544,13 +544,16 @@ async function deleteMediaFile(item, opts) {
|
|
|
544
544
|
const data = await api.getFile(path);
|
|
545
545
|
sha = data.sha;
|
|
546
546
|
}
|
|
547
|
-
await api.deleteFile({
|
|
547
|
+
const deleted = await api.deleteFile({
|
|
548
548
|
path,
|
|
549
549
|
sha,
|
|
550
550
|
message: `studio: delete ${path}`,
|
|
551
551
|
});
|
|
552
552
|
state.mediaBusy = false;
|
|
553
553
|
state.status = `Deleted ${path}`;
|
|
554
|
+
if (typeof opts.onCommitted === "function") {
|
|
555
|
+
opts.onCommitted(deleted?.commit);
|
|
556
|
+
}
|
|
554
557
|
await openMediaDir(state.mediaPath, opts);
|
|
555
558
|
} catch (err) {
|
|
556
559
|
state.mediaBusy = false;
|
|
@@ -600,12 +603,15 @@ async function createFolder(opts) {
|
|
|
600
603
|
state.status = `Creating ${name}…`;
|
|
601
604
|
render();
|
|
602
605
|
try {
|
|
603
|
-
await api.createMediaFolder({
|
|
606
|
+
const created = await api.createMediaFolder({
|
|
604
607
|
path,
|
|
605
608
|
message: `studio: mkdir ${path}`,
|
|
606
609
|
});
|
|
607
610
|
state.mediaBusy = false;
|
|
608
611
|
state.status = `Created ${toPublicPath(path) || path}`;
|
|
612
|
+
if (typeof opts.onCommitted === "function") {
|
|
613
|
+
opts.onCommitted(created?.commit);
|
|
614
|
+
}
|
|
609
615
|
await openMediaDir(state.mediaPath, opts);
|
|
610
616
|
} catch (err) {
|
|
611
617
|
state.mediaBusy = false;
|
|
@@ -619,6 +625,7 @@ async function uploadFiles(files, opts) {
|
|
|
619
625
|
clearError();
|
|
620
626
|
const dir = state.mediaPath || MEDIA_ROOT;
|
|
621
627
|
let ok = 0;
|
|
628
|
+
let lastCommit = null;
|
|
622
629
|
state.mediaBusy = true;
|
|
623
630
|
|
|
624
631
|
for (const file of files) {
|
|
@@ -643,6 +650,7 @@ async function uploadFiles(files, opts) {
|
|
|
643
650
|
});
|
|
644
651
|
ok += 1;
|
|
645
652
|
state.status = `Uploaded ${result.public_path || toPublicPath(path)}`;
|
|
653
|
+
lastCommit = result?.commit || lastCommit;
|
|
646
654
|
} catch (err) {
|
|
647
655
|
captureError(err);
|
|
648
656
|
state.mediaBusy = false;
|
|
@@ -653,5 +661,8 @@ async function uploadFiles(files, opts) {
|
|
|
653
661
|
|
|
654
662
|
state.mediaBusy = false;
|
|
655
663
|
if (ok) state.status = ok === 1 ? state.status : `Uploaded ${ok} files`;
|
|
664
|
+
if (ok && lastCommit && typeof opts.onCommitted === "function") {
|
|
665
|
+
opts.onCommitted(lastCommit);
|
|
666
|
+
}
|
|
656
667
|
await openMediaDir(dir, opts);
|
|
657
668
|
}
|
|
@@ -719,6 +719,67 @@ button.studio-brand-home.studio-brand-toolbar[aria-current="page"] {
|
|
|
719
719
|
background: color-mix(in srgb, #746f30 45%, var(--panel));
|
|
720
720
|
}
|
|
721
721
|
|
|
722
|
+
/* Content-pipeline status chip (production) / Export sync colors (local). */
|
|
723
|
+
.pipeline-chip {
|
|
724
|
+
cursor: default;
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
.pipeline-chip:not(:disabled) {
|
|
728
|
+
cursor: pointer;
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
.pipeline-awaiting,
|
|
732
|
+
.btn-reexport.pipeline-running,
|
|
733
|
+
.pipeline-running {
|
|
734
|
+
color: #5c4b00;
|
|
735
|
+
border-color: color-mix(in srgb, #e0a100 55%, var(--outline-variant));
|
|
736
|
+
background: color-mix(in srgb, #f5c84c 50%, var(--background));
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
.pipeline-ok,
|
|
740
|
+
.btn-reexport.pipeline-ok {
|
|
741
|
+
color: #14532d;
|
|
742
|
+
border-color: color-mix(in srgb, #22c55e 50%, var(--outline-variant));
|
|
743
|
+
background: color-mix(in srgb, #86efac 45%, var(--background));
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
.pipeline-error,
|
|
747
|
+
.btn-reexport.pipeline-error,
|
|
748
|
+
.pipeline-stalled {
|
|
749
|
+
color: #7f1d1d;
|
|
750
|
+
border-color: color-mix(in srgb, #ef4444 50%, var(--outline-variant));
|
|
751
|
+
background: color-mix(in srgb, #fca5a5 40%, var(--background));
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
.pipeline-idle {
|
|
755
|
+
color: var(--on-surface-variant);
|
|
756
|
+
border-color: var(--outline-variant);
|
|
757
|
+
background: var(--surface-container);
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
[data-theme="dark"] .pipeline-awaiting,
|
|
761
|
+
[data-theme="dark"] .pipeline-running,
|
|
762
|
+
[data-theme="dark"] .btn-reexport.pipeline-running {
|
|
763
|
+
color: #f5e6a3;
|
|
764
|
+
border-color: color-mix(in srgb, #e0a100 45%, var(--outline-variant));
|
|
765
|
+
background: color-mix(in srgb, #746f30 50%, var(--panel));
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
[data-theme="dark"] .pipeline-ok,
|
|
769
|
+
[data-theme="dark"] .btn-reexport.pipeline-ok {
|
|
770
|
+
color: #bbf7d0;
|
|
771
|
+
border-color: color-mix(in srgb, #22c55e 45%, var(--outline-variant));
|
|
772
|
+
background: color-mix(in srgb, #14532d 55%, var(--panel));
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
[data-theme="dark"] .pipeline-error,
|
|
776
|
+
[data-theme="dark"] .btn-reexport.pipeline-error,
|
|
777
|
+
[data-theme="dark"] .pipeline-stalled {
|
|
778
|
+
color: #fecaca;
|
|
779
|
+
border-color: color-mix(in srgb, #ef4444 45%, var(--outline-variant));
|
|
780
|
+
background: color-mix(in srgb, #7f1d1d 55%, var(--panel));
|
|
781
|
+
}
|
|
782
|
+
|
|
722
783
|
.studio-icon-bar .btn-icon {
|
|
723
784
|
width: var(--studio-list-control-height);
|
|
724
785
|
min-width: var(--studio-list-control-height);
|
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.
|
|
4
|
+
version: 1.4.5
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- The Pandorga contributors
|
|
@@ -449,8 +449,8 @@ files:
|
|
|
449
449
|
- studio-app/src/preview/render.js
|
|
450
450
|
- studio-app/src/styles/studio.css
|
|
451
451
|
- studio-app/vite.config.js
|
|
452
|
-
- studio/assets/index-
|
|
453
|
-
- studio/assets/index-
|
|
452
|
+
- studio/assets/index-Cd1mlY_H.css
|
|
453
|
+
- studio/assets/index-OAmrbW7i.js
|
|
454
454
|
- studio/index.html
|
|
455
455
|
- studio/schema.yml
|
|
456
456
|
- templates/articles/README.md
|