pressship 0.1.13 → 0.1.14
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/assets/web/app.js +1003 -106
- package/assets/web/style.css +2416 -964
- package/dist/web/release.js +10 -6
- package/dist/web/release.js.map +1 -1
- package/dist/web/server.d.ts +7 -0
- package/dist/web/server.js +76 -42
- package/dist/web/server.js.map +1 -1
- package/dist/web/version-state.js +14 -10
- package/dist/web/version-state.js.map +1 -1
- package/package.json +1 -1
package/assets/web/app.js
CHANGED
|
@@ -11,9 +11,11 @@ void import("/vendor/marked.esm.js")
|
|
|
11
11
|
breaks: true
|
|
12
12
|
});
|
|
13
13
|
markdownParser = (markdown) => marked.parse(markdown, { async: false });
|
|
14
|
+
refreshStudioAiMarkdownIfReady();
|
|
14
15
|
})
|
|
15
16
|
.catch(() => {
|
|
16
17
|
markdownParser = basicMarkdownToHtml;
|
|
18
|
+
refreshStudioAiMarkdownIfReady();
|
|
17
19
|
});
|
|
18
20
|
|
|
19
21
|
const MARKDOWN_ALLOWED_TAGS = new Set([
|
|
@@ -120,6 +122,8 @@ function clampStudioLayoutValue(key, value) {
|
|
|
120
122
|
}
|
|
121
123
|
|
|
122
124
|
const STUDIO_SIDEBAR_TAB_KEY = "pressship.studio.sidebar.tab.v1";
|
|
125
|
+
const STUDIO_PANEL_STORAGE_KEY = "pressship.studio.panels.v1";
|
|
126
|
+
const STUDIO_THEME_STORAGE_KEY = "pressship.studio.theme.v1";
|
|
123
127
|
|
|
124
128
|
function loadStudioSidebarTab(pluginKey) {
|
|
125
129
|
if (!pluginKey) {
|
|
@@ -147,6 +151,60 @@ function saveStudioSidebarTab(pluginKey, tab) {
|
|
|
147
151
|
}
|
|
148
152
|
}
|
|
149
153
|
|
|
154
|
+
function loadStudioPanelState() {
|
|
155
|
+
try {
|
|
156
|
+
const raw = typeof localStorage !== "undefined" ? localStorage.getItem(STUDIO_PANEL_STORAGE_KEY) : null;
|
|
157
|
+
const parsed = raw ? JSON.parse(raw) : {};
|
|
158
|
+
return normalizeStudioPanelState(parsed);
|
|
159
|
+
} catch {
|
|
160
|
+
return normalizeStudioPanelState();
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function normalizeStudioPanelState(value = {}) {
|
|
165
|
+
return {
|
|
166
|
+
files: value.files !== false,
|
|
167
|
+
sidebar: value.sidebar !== false
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function saveStudioPanelState(panels) {
|
|
172
|
+
try {
|
|
173
|
+
localStorage.setItem(STUDIO_PANEL_STORAGE_KEY, JSON.stringify(normalizeStudioPanelState(panels)));
|
|
174
|
+
} catch {
|
|
175
|
+
// ignore
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function normalizeStudioTheme(value) {
|
|
180
|
+
return value === "light" ? "light" : "dark";
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function loadStudioTheme() {
|
|
184
|
+
try {
|
|
185
|
+
const raw = typeof localStorage !== "undefined" ? localStorage.getItem(STUDIO_THEME_STORAGE_KEY) : null;
|
|
186
|
+
return normalizeStudioTheme(raw);
|
|
187
|
+
} catch {
|
|
188
|
+
return "dark";
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function saveStudioTheme(theme) {
|
|
193
|
+
try {
|
|
194
|
+
localStorage.setItem(STUDIO_THEME_STORAGE_KEY, normalizeStudioTheme(theme));
|
|
195
|
+
} catch {
|
|
196
|
+
// ignore
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function studioTheme() {
|
|
201
|
+
return normalizeStudioTheme(state.studio.theme);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function studioMonacoTheme() {
|
|
205
|
+
return studioTheme() === "light" ? "vs" : "vs-dark";
|
|
206
|
+
}
|
|
207
|
+
|
|
150
208
|
function createInitialStudioRelease() {
|
|
151
209
|
return {
|
|
152
210
|
tags: null,
|
|
@@ -359,6 +417,7 @@ const state = {
|
|
|
359
417
|
playgroundUrl: "",
|
|
360
418
|
playgroundUrls: null,
|
|
361
419
|
activeTab: "editor",
|
|
420
|
+
openFiles: [],
|
|
362
421
|
terminalOpen: true,
|
|
363
422
|
collapsedFolders: new Set(),
|
|
364
423
|
terminal: [],
|
|
@@ -374,6 +433,8 @@ const state = {
|
|
|
374
433
|
editorKind: null,
|
|
375
434
|
editorModels: [],
|
|
376
435
|
layout: loadStudioLayout(),
|
|
436
|
+
panels: loadStudioPanelState(),
|
|
437
|
+
theme: loadStudioTheme(),
|
|
377
438
|
sidebarTab: "ai",
|
|
378
439
|
playgroundVersionModal: null,
|
|
379
440
|
release: createInitialStudioRelease(),
|
|
@@ -423,6 +484,27 @@ const els = {
|
|
|
423
484
|
const isMac =
|
|
424
485
|
typeof navigator !== "undefined" && /Mac|iPhone|iPad|iPod/i.test(navigator.platform || "");
|
|
425
486
|
let monacoPromise = null;
|
|
487
|
+
const VIEW_ROUTE_PATHS = {
|
|
488
|
+
dashboard: "/dashboard",
|
|
489
|
+
studio: "/studio",
|
|
490
|
+
remote: "/wordpress.org",
|
|
491
|
+
local: "/local",
|
|
492
|
+
release: "/release",
|
|
493
|
+
settings: "/settings"
|
|
494
|
+
};
|
|
495
|
+
const ROUTE_VIEW_ALIASES = {
|
|
496
|
+
"": "dashboard",
|
|
497
|
+
dashboard: "dashboard",
|
|
498
|
+
studio: "studio",
|
|
499
|
+
"wordpress.org": "remote",
|
|
500
|
+
remote: "remote",
|
|
501
|
+
local: "local",
|
|
502
|
+
release: "release",
|
|
503
|
+
settings: "settings"
|
|
504
|
+
};
|
|
505
|
+
let applyingLocationRoute = false;
|
|
506
|
+
let initialRouteLoaderVisible = false;
|
|
507
|
+
|
|
426
508
|
document.querySelectorAll("[data-kbd-mod]").forEach((node) => {
|
|
427
509
|
node.textContent = isMac ? "⌘" : "Ctrl+";
|
|
428
510
|
});
|
|
@@ -525,6 +607,242 @@ els.commandInput?.addEventListener("input", () => {
|
|
|
525
607
|
renderCommandPalette();
|
|
526
608
|
});
|
|
527
609
|
|
|
610
|
+
window.addEventListener("popstate", () => {
|
|
611
|
+
void applyLocationRoute({ replaceRoute: true });
|
|
612
|
+
});
|
|
613
|
+
|
|
614
|
+
function normalizeViewId(view) {
|
|
615
|
+
return Object.prototype.hasOwnProperty.call(VIEW_ROUTE_PATHS, view) ? view : "dashboard";
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
function normalizeBrowserPath(pathname = window.location.pathname) {
|
|
619
|
+
const trimmed = pathname.replace(/\/+$/, "");
|
|
620
|
+
return trimmed || "/";
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
function shouldShowInitialRouteLoader() {
|
|
624
|
+
const path = normalizeBrowserPath();
|
|
625
|
+
return path !== "/" && path !== VIEW_ROUTE_PATHS.dashboard;
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
function showInitialRouteLoader() {
|
|
629
|
+
if (!shouldShowInitialRouteLoader() || initialRouteLoaderVisible) {
|
|
630
|
+
return;
|
|
631
|
+
}
|
|
632
|
+
initialRouteLoaderVisible = true;
|
|
633
|
+
document.body.classList.add("is-initial-route-loading");
|
|
634
|
+
const loader = document.createElement("div");
|
|
635
|
+
loader.id = "initial-route-loader";
|
|
636
|
+
loader.className = "ps-initial-route-loader";
|
|
637
|
+
loader.setAttribute("role", "status");
|
|
638
|
+
loader.setAttribute("aria-live", "polite");
|
|
639
|
+
loader.innerHTML = `
|
|
640
|
+
<div class="ps-initial-route-loader-card">
|
|
641
|
+
<img src="/brand/pressship-symbol.png" alt="" />
|
|
642
|
+
<span class="dashicons dashicons-update" aria-hidden="true"></span>
|
|
643
|
+
<strong>Loading Pressship Studio</strong>
|
|
644
|
+
<p>Restoring the requested page...</p>
|
|
645
|
+
</div>
|
|
646
|
+
`;
|
|
647
|
+
document.body.append(loader);
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
function hideInitialRouteLoader() {
|
|
651
|
+
if (!initialRouteLoaderVisible) {
|
|
652
|
+
return;
|
|
653
|
+
}
|
|
654
|
+
initialRouteLoaderVisible = false;
|
|
655
|
+
document.body.classList.remove("is-initial-route-loading");
|
|
656
|
+
document.getElementById("initial-route-loader")?.remove();
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
function decodeRouteSegment(segment) {
|
|
660
|
+
try {
|
|
661
|
+
return decodeURIComponent(segment);
|
|
662
|
+
} catch {
|
|
663
|
+
return segment;
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
function encodeRouteSegments(value) {
|
|
668
|
+
return String(value ?? "")
|
|
669
|
+
.split("/")
|
|
670
|
+
.filter(Boolean)
|
|
671
|
+
.map((segment) => encodeURIComponent(segment))
|
|
672
|
+
.join("/");
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
function parseLocationRoute(pathname = window.location.pathname) {
|
|
676
|
+
const segments = pathname
|
|
677
|
+
.split("/")
|
|
678
|
+
.filter(Boolean)
|
|
679
|
+
.map((segment) => decodeRouteSegment(segment));
|
|
680
|
+
const head = segments[0] ?? "";
|
|
681
|
+
|
|
682
|
+
if (head === "studio") {
|
|
683
|
+
return {
|
|
684
|
+
view: "studio",
|
|
685
|
+
studio: {
|
|
686
|
+
project: segments[1] ?? "",
|
|
687
|
+
filePath: segments.slice(2).join("/")
|
|
688
|
+
}
|
|
689
|
+
};
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
return {
|
|
693
|
+
view: ROUTE_VIEW_ALIASES[head] ?? "dashboard"
|
|
694
|
+
};
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
function applyActiveViewShell(view) {
|
|
698
|
+
const nextView = normalizeViewId(view);
|
|
699
|
+
state.activeView = nextView;
|
|
700
|
+
document.body.dataset.activeView = nextView;
|
|
701
|
+
document.querySelectorAll(".view").forEach((node) => node.classList.remove("is-active"));
|
|
702
|
+
document.getElementById(`view-${nextView}`)?.classList.add("is-active");
|
|
703
|
+
document
|
|
704
|
+
.querySelectorAll("#adminmenu li")
|
|
705
|
+
.forEach((node) => node.classList.remove("wp-has-current-submenu"));
|
|
706
|
+
document
|
|
707
|
+
.querySelector(`#adminmenu li[data-view="${nextView}"]`)
|
|
708
|
+
?.classList.add("wp-has-current-submenu");
|
|
709
|
+
return nextView;
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
function primeInitialRouteState(route) {
|
|
713
|
+
const nextView = applyActiveViewShell(route.view);
|
|
714
|
+
if (nextView !== "studio" || !route.studio?.project || state.studio.id) {
|
|
715
|
+
return;
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
const filePath = route.studio.filePath;
|
|
719
|
+
state.studio = {
|
|
720
|
+
...state.studio,
|
|
721
|
+
scope: null,
|
|
722
|
+
id: route.studio.project,
|
|
723
|
+
plugin: { slug: route.studio.project, name: route.studio.project },
|
|
724
|
+
files: [],
|
|
725
|
+
selectedFile: filePath
|
|
726
|
+
? {
|
|
727
|
+
path: filePath,
|
|
728
|
+
name: filePath.split("/").pop() ?? filePath,
|
|
729
|
+
directory: filePath.includes("/") ? filePath.split("/").slice(0, -1).join("/") : "",
|
|
730
|
+
size: 0
|
|
731
|
+
}
|
|
732
|
+
: null,
|
|
733
|
+
fileContent: "",
|
|
734
|
+
draftContent: "",
|
|
735
|
+
readOnly: true,
|
|
736
|
+
dirty: false,
|
|
737
|
+
loading: true,
|
|
738
|
+
running: false,
|
|
739
|
+
checking: false,
|
|
740
|
+
jobId: null,
|
|
741
|
+
checkJobId: null,
|
|
742
|
+
activeTab: "editor",
|
|
743
|
+
openFiles: filePath ? [filePath] : [],
|
|
744
|
+
terminal: [`Opening ${route.studio.project} from URL...`],
|
|
745
|
+
collapsedFolders: new Set(),
|
|
746
|
+
pendingConfirms: new Map()
|
|
747
|
+
};
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
function studioProjectRouteSegment() {
|
|
751
|
+
const plugin = state.studio.plugin;
|
|
752
|
+
const localPlugin = state.local.find((item) => item.id === state.studio.id);
|
|
753
|
+
return plugin?.slug || localPlugin?.slug || plugin?.name || state.studio.id || "";
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
function studioRoutePathForState() {
|
|
757
|
+
if (!state.studio.id) {
|
|
758
|
+
return VIEW_ROUTE_PATHS.studio;
|
|
759
|
+
}
|
|
760
|
+
const project = studioProjectRouteSegment();
|
|
761
|
+
if (!project) {
|
|
762
|
+
return VIEW_ROUTE_PATHS.studio;
|
|
763
|
+
}
|
|
764
|
+
const filePath =
|
|
765
|
+
state.studio.activeTab === "editor" && state.studio.selectedFile?.path
|
|
766
|
+
? encodeRouteSegments(state.studio.selectedFile.path)
|
|
767
|
+
: "";
|
|
768
|
+
const projectPath = encodeURIComponent(project);
|
|
769
|
+
return filePath ? `/studio/${projectPath}/${filePath}` : `/studio/${projectPath}`;
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
function routePathForState() {
|
|
773
|
+
const view = normalizeViewId(state.activeView);
|
|
774
|
+
return view === "studio" ? studioRoutePathForState() : VIEW_ROUTE_PATHS[view];
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
function updateRouteFromState(options = {}) {
|
|
778
|
+
if (applyingLocationRoute && !options.force) {
|
|
779
|
+
return;
|
|
780
|
+
}
|
|
781
|
+
const nextPath = routePathForState();
|
|
782
|
+
if (normalizeBrowserPath(nextPath) === normalizeBrowserPath()) {
|
|
783
|
+
return;
|
|
784
|
+
}
|
|
785
|
+
const method = options.replace ? "replaceState" : "pushState";
|
|
786
|
+
history[method]({ pressshipView: state.activeView }, "", nextPath);
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
function resolveStudioRouteProject(project) {
|
|
790
|
+
if (!project) {
|
|
791
|
+
return null;
|
|
792
|
+
}
|
|
793
|
+
const normalizedProject = project.toLowerCase();
|
|
794
|
+
const matchesProject = (candidate) => {
|
|
795
|
+
if (!candidate) {
|
|
796
|
+
return false;
|
|
797
|
+
}
|
|
798
|
+
return candidate === project || candidate.toLowerCase() === normalizedProject;
|
|
799
|
+
};
|
|
800
|
+
const localPlugin = state.local.find((plugin) =>
|
|
801
|
+
[plugin.slug, plugin.id, plugin.name].some((candidate) => matchesProject(candidate))
|
|
802
|
+
);
|
|
803
|
+
if (localPlugin) {
|
|
804
|
+
return { scope: "local", id: localPlugin.id };
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
const remotePlugin = state.remote.find((plugin) =>
|
|
808
|
+
[plugin.slug, plugin.name].some((candidate) => matchesProject(candidate))
|
|
809
|
+
);
|
|
810
|
+
return { scope: "remote", id: remotePlugin?.slug || project };
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
async function applyLocationRoute(options = {}) {
|
|
814
|
+
const route = parseLocationRoute();
|
|
815
|
+
applyingLocationRoute = true;
|
|
816
|
+
try {
|
|
817
|
+
if (route.view === "studio") {
|
|
818
|
+
if (route.studio?.project) {
|
|
819
|
+
const target = resolveStudioRouteProject(route.studio.project);
|
|
820
|
+
if (target) {
|
|
821
|
+
await openStudio(target.scope, target.id, {
|
|
822
|
+
filePath: route.studio.filePath,
|
|
823
|
+
updateRoute: false
|
|
824
|
+
});
|
|
825
|
+
} else {
|
|
826
|
+
await showView("studio", { updateRoute: false });
|
|
827
|
+
renderStudio();
|
|
828
|
+
}
|
|
829
|
+
} else {
|
|
830
|
+
await showView("studio", { updateRoute: false });
|
|
831
|
+
renderStudio();
|
|
832
|
+
}
|
|
833
|
+
} else {
|
|
834
|
+
await showView(route.view, { updateRoute: false });
|
|
835
|
+
}
|
|
836
|
+
} finally {
|
|
837
|
+
applyingLocationRoute = false;
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
if (options.replaceRoute !== false) {
|
|
841
|
+
updateRouteFromState({ replace: true, force: true });
|
|
842
|
+
}
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
showInitialRouteLoader();
|
|
528
846
|
void boot();
|
|
529
847
|
|
|
530
848
|
async function boot() {
|
|
@@ -532,13 +850,15 @@ async function boot() {
|
|
|
532
850
|
state.bootstrap = await api("/api/bootstrap");
|
|
533
851
|
refreshTokenFromBootstrap(state.bootstrap);
|
|
534
852
|
} catch (error) {
|
|
853
|
+
hideInitialRouteLoader();
|
|
535
854
|
notice(`Could not load bootstrap state: ${error.message}`, "error");
|
|
536
855
|
return;
|
|
537
856
|
}
|
|
538
857
|
state.settings = state.bootstrap.settings ?? null;
|
|
539
858
|
state.playgrounds = state.bootstrap.playgrounds ?? [];
|
|
540
859
|
state.aiAssistance.harnesses = state.bootstrap.aiHarnesses ?? [];
|
|
541
|
-
|
|
860
|
+
const initialRoute = parseLocationRoute();
|
|
861
|
+
primeInitialRouteState(initialRoute);
|
|
542
862
|
renderAccount();
|
|
543
863
|
for (const job of state.bootstrap.jobs ?? []) {
|
|
544
864
|
upsertJob(job);
|
|
@@ -549,8 +869,16 @@ async function boot() {
|
|
|
549
869
|
renderDashboard();
|
|
550
870
|
renderStudio();
|
|
551
871
|
renderPlaygroundsMenu();
|
|
872
|
+
if (state.activeView === "release") {
|
|
873
|
+
void loadReleaseBoard();
|
|
874
|
+
}
|
|
552
875
|
void loadAiAssistance();
|
|
553
|
-
|
|
876
|
+
try {
|
|
877
|
+
await Promise.all([loadRemote(), loadLocal()]);
|
|
878
|
+
await applyLocationRoute({ replaceRoute: true });
|
|
879
|
+
} finally {
|
|
880
|
+
hideInitialRouteLoader();
|
|
881
|
+
}
|
|
554
882
|
}
|
|
555
883
|
|
|
556
884
|
function renderAccount() {
|
|
@@ -598,6 +926,10 @@ async function runAction(name, element) {
|
|
|
598
926
|
await selectStudioFile(element.dataset.path);
|
|
599
927
|
return;
|
|
600
928
|
|
|
929
|
+
case "studio-close-file-tab":
|
|
930
|
+
await closeStudioFileTab(element.dataset.path);
|
|
931
|
+
return;
|
|
932
|
+
|
|
601
933
|
case "studio-toggle-folder":
|
|
602
934
|
toggleStudioFolder(element.dataset.folder);
|
|
603
935
|
return;
|
|
@@ -606,6 +938,18 @@ async function runAction(name, element) {
|
|
|
606
938
|
toggleStudioTerminal();
|
|
607
939
|
return;
|
|
608
940
|
|
|
941
|
+
case "studio-toggle-files":
|
|
942
|
+
toggleStudioPanel("files");
|
|
943
|
+
return;
|
|
944
|
+
|
|
945
|
+
case "studio-toggle-sidebar":
|
|
946
|
+
toggleStudioPanel("sidebar");
|
|
947
|
+
return;
|
|
948
|
+
|
|
949
|
+
case "studio-open-sidebar-tab":
|
|
950
|
+
openStudioSidebarTab(element.dataset.tab);
|
|
951
|
+
return;
|
|
952
|
+
|
|
609
953
|
case "studio-save":
|
|
610
954
|
await saveStudioFile();
|
|
611
955
|
return;
|
|
@@ -614,6 +958,10 @@ async function runAction(name, element) {
|
|
|
614
958
|
await runStudioCheck();
|
|
615
959
|
return;
|
|
616
960
|
|
|
961
|
+
case "studio-toggle-theme":
|
|
962
|
+
toggleStudioTheme();
|
|
963
|
+
return;
|
|
964
|
+
|
|
617
965
|
case "studio-check-note":
|
|
618
966
|
revealStudioCheckNote(Number(element.dataset.line || 1), Number(element.dataset.column || 1));
|
|
619
967
|
return;
|
|
@@ -1106,6 +1454,7 @@ async function openStudio(scope, id, options = {}) {
|
|
|
1106
1454
|
playgroundUrl: "",
|
|
1107
1455
|
playgroundUrls: null,
|
|
1108
1456
|
activeTab: "editor",
|
|
1457
|
+
openFiles: [],
|
|
1109
1458
|
terminalOpen: true,
|
|
1110
1459
|
collapsedFolders: new Set(),
|
|
1111
1460
|
terminal: [`Pressship Studio opened for ${scope === "local" ? "local plugin" : "WordPress.org plugin"} ${id}.`],
|
|
@@ -1121,13 +1470,15 @@ async function openStudio(scope, id, options = {}) {
|
|
|
1121
1470
|
editorKind: null,
|
|
1122
1471
|
editorModels: [],
|
|
1123
1472
|
layout: state.studio.layout ?? loadStudioLayout(),
|
|
1473
|
+
panels: normalizeStudioPanelState(state.studio.panels ?? loadStudioPanelState()),
|
|
1474
|
+
theme: normalizeStudioTheme(state.studio.theme ?? loadStudioTheme()),
|
|
1124
1475
|
sidebarTab,
|
|
1125
1476
|
playgroundVersionModal: null,
|
|
1126
1477
|
release: createInitialStudioRelease(),
|
|
1127
1478
|
pendingConfirms: new Map()
|
|
1128
1479
|
};
|
|
1129
1480
|
saveStudioSidebarTab(pluginKey, sidebarTab);
|
|
1130
|
-
showView("studio");
|
|
1481
|
+
await showView("studio", { updateRoute: false });
|
|
1131
1482
|
renderStudio();
|
|
1132
1483
|
if (scope === "local" && sidebarTab === "release") {
|
|
1133
1484
|
void loadStudioReleaseTags();
|
|
@@ -1146,26 +1497,45 @@ async function openStudio(scope, id, options = {}) {
|
|
|
1146
1497
|
state.studio.files = result.files ?? [];
|
|
1147
1498
|
applyStudioCheckState(checkState.state);
|
|
1148
1499
|
renderStudio();
|
|
1149
|
-
const
|
|
1500
|
+
const requestedFile = options.filePath
|
|
1501
|
+
? state.studio.files.find((file) => file.path === options.filePath)
|
|
1502
|
+
: null;
|
|
1503
|
+
if (options.filePath && !requestedFile) {
|
|
1504
|
+
appendStudioTerminal(`Route file not found: ${options.filePath}`, "warning");
|
|
1505
|
+
}
|
|
1506
|
+
const initialFile = requestedFile ?? chooseInitialStudioFile(state.studio.files, state.studio.plugin?.slug);
|
|
1150
1507
|
if (initialFile) {
|
|
1151
|
-
await selectStudioFile(initialFile.path
|
|
1508
|
+
await selectStudioFile(initialFile.path, {
|
|
1509
|
+
updateRoute: options.updateRoute,
|
|
1510
|
+
replaceRoute: options.replaceRoute
|
|
1511
|
+
});
|
|
1152
1512
|
} else {
|
|
1153
1513
|
state.studio.draftContent = "";
|
|
1154
1514
|
remountStudioEditorIfNeeded();
|
|
1515
|
+
if (options.updateRoute !== false) {
|
|
1516
|
+
updateRouteFromState({ replace: options.replaceRoute });
|
|
1517
|
+
}
|
|
1155
1518
|
}
|
|
1156
1519
|
} else {
|
|
1157
1520
|
state.studio.files = [{ path: "readme.txt", name: "readme.txt", directory: "", size: detail.readme?.length ?? 0 }];
|
|
1158
1521
|
state.studio.selectedFile = state.studio.files[0];
|
|
1522
|
+
state.studio.openFiles = ["readme.txt"];
|
|
1159
1523
|
state.studio.fileContent = detail.readme ?? "No hosted readme.txt could be loaded.";
|
|
1160
1524
|
state.studio.draftContent = state.studio.fileContent;
|
|
1161
1525
|
state.studio.readOnly = true;
|
|
1162
1526
|
renderStudio();
|
|
1163
1527
|
remountStudioEditorIfNeeded();
|
|
1528
|
+
if (options.updateRoute !== false) {
|
|
1529
|
+
updateRouteFromState({ replace: options.replaceRoute });
|
|
1530
|
+
}
|
|
1164
1531
|
}
|
|
1165
1532
|
} catch (error) {
|
|
1166
1533
|
state.studio.loading = false;
|
|
1167
1534
|
appendStudioTerminal(error.message, "error");
|
|
1168
1535
|
renderStudio();
|
|
1536
|
+
if (options.updateRoute !== false) {
|
|
1537
|
+
updateRouteFromState({ replace: options.replaceRoute });
|
|
1538
|
+
}
|
|
1169
1539
|
}
|
|
1170
1540
|
}
|
|
1171
1541
|
|
|
@@ -1188,6 +1558,7 @@ async function selectStudioFile(relativePath, options = {}) {
|
|
|
1188
1558
|
return;
|
|
1189
1559
|
}
|
|
1190
1560
|
|
|
1561
|
+
ensureStudioFileTab(relativePath);
|
|
1191
1562
|
state.studio.selectedFile = state.studio.files.find((file) => file.path === relativePath) ?? {
|
|
1192
1563
|
path: relativePath,
|
|
1193
1564
|
name: relativePath.split("/").pop() ?? relativePath,
|
|
@@ -1200,6 +1571,9 @@ async function selectStudioFile(relativePath, options = {}) {
|
|
|
1200
1571
|
state.studio.activeTab = "editor";
|
|
1201
1572
|
renderStudio();
|
|
1202
1573
|
remountStudioEditorIfNeeded();
|
|
1574
|
+
if (options.updateRoute !== false) {
|
|
1575
|
+
updateRouteFromState({ replace: options.replaceRoute });
|
|
1576
|
+
}
|
|
1203
1577
|
|
|
1204
1578
|
try {
|
|
1205
1579
|
const result = await api(
|
|
@@ -1217,6 +1591,17 @@ async function selectStudioFile(relativePath, options = {}) {
|
|
|
1217
1591
|
}
|
|
1218
1592
|
}
|
|
1219
1593
|
|
|
1594
|
+
function ensureStudioFileTab(relativePath) {
|
|
1595
|
+
if (!relativePath) {
|
|
1596
|
+
return;
|
|
1597
|
+
}
|
|
1598
|
+
const openFiles = Array.isArray(state.studio.openFiles) ? state.studio.openFiles : [];
|
|
1599
|
+
if (!openFiles.includes(relativePath)) {
|
|
1600
|
+
openFiles.push(relativePath);
|
|
1601
|
+
}
|
|
1602
|
+
state.studio.openFiles = openFiles;
|
|
1603
|
+
}
|
|
1604
|
+
|
|
1220
1605
|
async function saveStudioFile() {
|
|
1221
1606
|
if (state.studio.scope !== "local" || !state.studio.id || !state.studio.selectedFile) {
|
|
1222
1607
|
return;
|
|
@@ -1472,6 +1857,7 @@ async function selectStudioAiChange(filePath) {
|
|
|
1472
1857
|
state.studio.activeTab = "editor";
|
|
1473
1858
|
renderStudio();
|
|
1474
1859
|
remountStudioEditorIfNeeded();
|
|
1860
|
+
updateRouteFromState();
|
|
1475
1861
|
updateStudioAiSidebar();
|
|
1476
1862
|
}
|
|
1477
1863
|
|
|
@@ -1571,6 +1957,35 @@ function renderStudio() {
|
|
|
1571
1957
|
? "WordPress.org plugin"
|
|
1572
1958
|
: "Choose a plugin from WordPress.org or Local Library.";
|
|
1573
1959
|
|
|
1960
|
+
if (state.studio.loading && !state.studio.scope && state.studio.id) {
|
|
1961
|
+
const fileLabel = state.studio.selectedFile?.path
|
|
1962
|
+
? `Restoring ${state.studio.selectedFile.path}.`
|
|
1963
|
+
: "Restoring the workspace.";
|
|
1964
|
+
els.studio.innerHTML = `
|
|
1965
|
+
<div class="studio-root studio-empty-root">
|
|
1966
|
+
<div class="studio-empty-state" aria-label="Opening Studio workspace">
|
|
1967
|
+
<section class="studio-empty-copy">
|
|
1968
|
+
<span class="studio-empty-kicker">
|
|
1969
|
+
<span class="dashicons dashicons-editor-code" aria-hidden="true"></span>
|
|
1970
|
+
Pressship Studio
|
|
1971
|
+
</span>
|
|
1972
|
+
<h1>Opening ${escapeHtml(title)}</h1>
|
|
1973
|
+
<p class="studio-empty-subtitle">${escapeHtml(fileLabel)}</p>
|
|
1974
|
+
</section>
|
|
1975
|
+
<section class="studio-empty-panel" aria-label="Loading workspace">
|
|
1976
|
+
${loadingShell("Loading Studio workspace...")}
|
|
1977
|
+
</section>
|
|
1978
|
+
<div class="studio-empty-footer">
|
|
1979
|
+
<span class="dashicons dashicons-update" aria-hidden="true"></span>
|
|
1980
|
+
Matching the URL to a local or WordPress.org plugin.
|
|
1981
|
+
</div>
|
|
1982
|
+
</div>
|
|
1983
|
+
</div>
|
|
1984
|
+
`;
|
|
1985
|
+
updateStudioControls();
|
|
1986
|
+
return;
|
|
1987
|
+
}
|
|
1988
|
+
|
|
1574
1989
|
if (!state.studio.id) {
|
|
1575
1990
|
const pickerOptions = studioPickerOptions();
|
|
1576
1991
|
const pickerDisabled = pickerOptions.length ? "" : "disabled";
|
|
@@ -1635,21 +2050,32 @@ function renderStudio() {
|
|
|
1635
2050
|
const fileList = state.studio.files.length
|
|
1636
2051
|
? renderStudioFileTree(buildStudioFileTree(state.studio.files))
|
|
1637
2052
|
: `<p class="studio-muted">No editable text files found.</p>`;
|
|
1638
|
-
const editorTabLabel = state.studio.selectedFile?.path ?? "Editor";
|
|
1639
2053
|
const playgroundPort = state.studio.playgroundUrl ? new URL(state.studio.playgroundUrl).port : "";
|
|
2054
|
+
const panels = normalizeStudioPanelState(state.studio.panels);
|
|
1640
2055
|
|
|
1641
2056
|
els.studio.innerHTML = `
|
|
1642
|
-
<div class="studio-root${state.studio.terminalOpen ? " has-terminal" : ""}">
|
|
2057
|
+
<div class="studio-root${state.studio.terminalOpen ? " has-terminal" : ""}${panels.files ? " has-files" : " is-files-collapsed"}${panels.sidebar ? " has-secondary-sidebar" : " is-secondary-sidebar-collapsed"}" data-theme="${escapeAttr(studioTheme())}">
|
|
1643
2058
|
<header class="studio-titlebar">
|
|
1644
2059
|
<div class="studio-title">
|
|
1645
2060
|
<strong>${escapeHtml(title)}</strong>
|
|
1646
2061
|
<span>${escapeHtml(source)}</span>
|
|
1647
2062
|
</div>
|
|
1648
2063
|
<div class="studio-title-actions">
|
|
2064
|
+
<button class="studio-layout-button${panels.files ? " is-active" : ""}" type="button" data-action="studio-toggle-files" aria-pressed="${panels.files ? "true" : "false"}" title="${panels.files ? "Hide Explorer" : "Show Explorer"}">
|
|
2065
|
+
<span class="dashicons dashicons-align-left" aria-hidden="true"></span>
|
|
2066
|
+
</button>
|
|
2067
|
+
${renderStudioPlayButton()}
|
|
2068
|
+
<span class="studio-preview-state${state.studio.running ? " is-loading" : state.studio.playgroundUrl ? " is-ready" : ""}">
|
|
2069
|
+
<span aria-hidden="true"></span>
|
|
2070
|
+
${escapeHtml(studioPreviewStateLabel())}
|
|
2071
|
+
</span>
|
|
1649
2072
|
<button class="studio-icon-button" type="button" data-action="studio-toggle-terminal" aria-pressed="${state.studio.terminalOpen ? "true" : "false"}" title="${state.studio.terminalOpen ? "Hide terminal" : "Show terminal"}">
|
|
1650
2073
|
<span class="dashicons dashicons-editor-kitchensink" aria-hidden="true"></span>
|
|
1651
2074
|
<span>Terminal</span>
|
|
1652
2075
|
</button>
|
|
2076
|
+
<button class="studio-layout-button${panels.sidebar ? " is-active" : ""}" type="button" data-action="studio-toggle-sidebar" aria-pressed="${panels.sidebar ? "true" : "false"}" title="${panels.sidebar ? "Hide Secondary Side Bar" : "Show Secondary Side Bar"}">
|
|
2077
|
+
<span class="dashicons dashicons-align-right" aria-hidden="true"></span>
|
|
2078
|
+
</button>
|
|
1653
2079
|
<button class="studio-action-button" type="button" data-action="studio-save" id="studio-save-button" disabled>
|
|
1654
2080
|
<span class="dashicons dashicons-saved" aria-hidden="true"></span>
|
|
1655
2081
|
Save
|
|
@@ -1658,37 +2084,31 @@ function renderStudio() {
|
|
|
1658
2084
|
<span class="dashicons dashicons-yes-alt" aria-hidden="true"></span>
|
|
1659
2085
|
Check
|
|
1660
2086
|
</button>
|
|
2087
|
+
${renderStudioThemeToggle()}
|
|
1661
2088
|
</div>
|
|
1662
2089
|
</header>
|
|
1663
2090
|
<div class="studio-main">
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
2091
|
+
${renderStudioActivityBar(panels)}
|
|
2092
|
+
${
|
|
2093
|
+
panels.files
|
|
2094
|
+
? `<aside class="studio-files" aria-label="Explorer">
|
|
2095
|
+
<header class="studio-pane-header">
|
|
2096
|
+
<strong>Explorer</strong>
|
|
2097
|
+
<button class="studio-pane-action" type="button" data-action="studio-toggle-files" aria-label="Hide Explorer" title="Hide Explorer">
|
|
2098
|
+
<span class="dashicons dashicons-no-alt" aria-hidden="true"></span>
|
|
2099
|
+
</button>
|
|
2100
|
+
</header>
|
|
2101
|
+
<div class="studio-file-list">${fileList}</div>
|
|
2102
|
+
</aside>
|
|
2103
|
+
${renderStudioResizer("files", "h", { label: "Explorer" })}`
|
|
2104
|
+
: ""
|
|
2105
|
+
}
|
|
1668
2106
|
<section class="studio-workbench" aria-label="Studio editor">
|
|
1669
2107
|
<div class="studio-tabs" aria-label="Studio tabs and Playground controls">
|
|
1670
2108
|
<div class="studio-tablist" role="tablist" aria-label="Studio tabs">
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
<span>${escapeHtml(editorTabLabel)}</span>
|
|
1674
|
-
${state.studio.dirty ? `<em aria-label="Unsaved changes"></em>` : ""}
|
|
1675
|
-
</button>
|
|
1676
|
-
<button type="button" role="tab" aria-selected="${state.studio.activeTab === "home" ? "true" : "false"}" class="studio-tab-button studio-preview-tab${state.studio.activeTab === "home" ? " is-active" : ""}" data-action="studio-tab" data-tab="home">
|
|
1677
|
-
<span class="dashicons dashicons-admin-home" aria-hidden="true"></span>
|
|
1678
|
-
<span>Home</span>
|
|
1679
|
-
${playgroundPort ? `<small>${escapeHtml(`:${playgroundPort}`)}</small>` : ""}
|
|
1680
|
-
</button>
|
|
1681
|
-
<button type="button" role="tab" aria-selected="${state.studio.activeTab === "admin" ? "true" : "false"}" class="studio-tab-button studio-preview-tab${state.studio.activeTab === "admin" ? " is-active" : ""}" data-action="studio-tab" data-tab="admin">
|
|
1682
|
-
<span class="dashicons dashicons-admin-site-alt3" aria-hidden="true"></span>
|
|
1683
|
-
<span>WP Admin</span>
|
|
1684
|
-
${state.studio.playgroundUrl ? `<small>admin/password</small>` : ""}
|
|
1685
|
-
</button>
|
|
2109
|
+
${renderStudioPinnedPreviewTabs(playgroundPort)}
|
|
2110
|
+
${renderStudioFileTabs()}
|
|
1686
2111
|
</div>
|
|
1687
|
-
${renderStudioPlayButton()}
|
|
1688
|
-
<span class="studio-preview-state${state.studio.running ? " is-loading" : state.studio.playgroundUrl ? " is-ready" : ""}">
|
|
1689
|
-
<span aria-hidden="true"></span>
|
|
1690
|
-
${escapeHtml(studioPreviewStateLabel())}
|
|
1691
|
-
</span>
|
|
1692
2112
|
<span class="studio-tab-spacer"></span>
|
|
1693
2113
|
<span id="studio-editor-status">${escapeHtml(studioEditorStatusLabel())}</span>
|
|
1694
2114
|
</div>
|
|
@@ -1706,23 +2126,202 @@ function renderStudio() {
|
|
|
1706
2126
|
<div id="studio-terminal-output" class="studio-terminal-output">
|
|
1707
2127
|
${renderStudioTerminal()}
|
|
1708
2128
|
</div>
|
|
1709
|
-
|
|
2129
|
+
</section>`
|
|
1710
2130
|
: ""
|
|
1711
2131
|
}
|
|
1712
2132
|
</section>
|
|
1713
|
-
${
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
2133
|
+
${
|
|
2134
|
+
panels.sidebar
|
|
2135
|
+
? `${renderStudioResizer("ai", "h", { invert: true, label: "Secondary Side Bar" })}
|
|
2136
|
+
<aside class="studio-ai" id="studio-ai" aria-label="Secondary Side Bar">
|
|
2137
|
+
${renderStudioAiSidebar()}
|
|
2138
|
+
</aside>`
|
|
2139
|
+
: ""
|
|
2140
|
+
}
|
|
1717
2141
|
</div>
|
|
2142
|
+
${renderStudioStatusBar()}
|
|
1718
2143
|
${renderStudioPlaygroundVersionModal()}
|
|
1719
2144
|
</div>
|
|
1720
2145
|
`;
|
|
1721
2146
|
applyStudioLayout(els.studio.querySelector(".studio-root"));
|
|
1722
2147
|
bindStudioResizers();
|
|
2148
|
+
scrollActiveStudioFileTabIntoView();
|
|
1723
2149
|
updateStudioControls();
|
|
1724
2150
|
}
|
|
1725
2151
|
|
|
2152
|
+
function renderStudioActivityBar(panels) {
|
|
2153
|
+
const tab = state.studio.sidebarTab === "release" ? "release" : "ai";
|
|
2154
|
+
const activityButton = ({ action, icon, label, active, tab: targetTab }) => `
|
|
2155
|
+
<button class="studio-activity-button${active ? " is-active" : ""}" type="button" data-action="${escapeAttr(action)}"${targetTab ? ` data-tab="${escapeAttr(targetTab)}"` : ""} aria-pressed="${active ? "true" : "false"}" aria-label="${escapeAttr(label)}" title="${escapeAttr(label)}">
|
|
2156
|
+
<span class="dashicons ${escapeAttr(icon)}" aria-hidden="true"></span>
|
|
2157
|
+
</button>
|
|
2158
|
+
`;
|
|
2159
|
+
|
|
2160
|
+
return `
|
|
2161
|
+
<nav class="studio-activitybar" aria-label="Studio workbench views">
|
|
2162
|
+
<div class="studio-activitybar-primary">
|
|
2163
|
+
${activityButton({
|
|
2164
|
+
action: "studio-toggle-files",
|
|
2165
|
+
icon: "dashicons-open-folder",
|
|
2166
|
+
label: panels.files ? "Hide Explorer" : "Show Explorer",
|
|
2167
|
+
active: panels.files
|
|
2168
|
+
})}
|
|
2169
|
+
${activityButton({
|
|
2170
|
+
action: "studio-tab",
|
|
2171
|
+
icon: "dashicons-editor-code",
|
|
2172
|
+
label: "Editor",
|
|
2173
|
+
active: state.studio.activeTab === "editor",
|
|
2174
|
+
tab: "editor"
|
|
2175
|
+
})}
|
|
2176
|
+
${activityButton({
|
|
2177
|
+
action: "studio-tab",
|
|
2178
|
+
icon: "dashicons-admin-home",
|
|
2179
|
+
label: "Playground Home",
|
|
2180
|
+
active: state.studio.activeTab === "home",
|
|
2181
|
+
tab: "home"
|
|
2182
|
+
})}
|
|
2183
|
+
</div>
|
|
2184
|
+
<div class="studio-activitybar-secondary">
|
|
2185
|
+
${activityButton({
|
|
2186
|
+
action: "studio-open-sidebar-tab",
|
|
2187
|
+
icon: "dashicons-format-chat",
|
|
2188
|
+
label: "AI Helper",
|
|
2189
|
+
active: panels.sidebar && tab === "ai",
|
|
2190
|
+
tab: "ai"
|
|
2191
|
+
})}
|
|
2192
|
+
${activityButton({
|
|
2193
|
+
action: "studio-open-sidebar-tab",
|
|
2194
|
+
icon: "dashicons-update",
|
|
2195
|
+
label: "Release",
|
|
2196
|
+
active: panels.sidebar && tab === "release",
|
|
2197
|
+
tab: "release"
|
|
2198
|
+
})}
|
|
2199
|
+
${activityButton({
|
|
2200
|
+
action: "studio-toggle-terminal",
|
|
2201
|
+
icon: "dashicons-editor-kitchensink",
|
|
2202
|
+
label: state.studio.terminalOpen ? "Hide Terminal" : "Show Terminal",
|
|
2203
|
+
active: state.studio.terminalOpen
|
|
2204
|
+
})}
|
|
2205
|
+
</div>
|
|
2206
|
+
</nav>
|
|
2207
|
+
`;
|
|
2208
|
+
}
|
|
2209
|
+
|
|
2210
|
+
function renderStudioStatusBar() {
|
|
2211
|
+
const plugin = state.studio.plugin;
|
|
2212
|
+
const file = state.studio.selectedFile?.path ?? "No file selected";
|
|
2213
|
+
const check = state.studio.checkSummary
|
|
2214
|
+
? `${state.studio.checkSummary.error || 0} errors, ${state.studio.checkSummary.warning || 0} warnings`
|
|
2215
|
+
: "Plugin Check idle";
|
|
2216
|
+
const assistant = selectedStudioAiAssistant();
|
|
2217
|
+
return `
|
|
2218
|
+
<footer class="studio-statusbar" aria-label="Studio status">
|
|
2219
|
+
<span><span class="dashicons dashicons-admin-plugins" aria-hidden="true"></span>${escapeHtml(plugin?.slug ?? state.studio.id ?? "Studio")}</span>
|
|
2220
|
+
<span>${escapeHtml(file)}</span>
|
|
2221
|
+
<span>${escapeHtml(check)}</span>
|
|
2222
|
+
<span class="studio-statusbar-spacer"></span>
|
|
2223
|
+
<span>${escapeHtml(assistant === "none" ? "AI disabled" : assistantLabel(assistant))}</span>
|
|
2224
|
+
<span>${escapeHtml(state.studio.readOnly ? "Read-only" : "Writable")}</span>
|
|
2225
|
+
</footer>
|
|
2226
|
+
`;
|
|
2227
|
+
}
|
|
2228
|
+
|
|
2229
|
+
function renderStudioPinnedPreviewTabs(playgroundPort) {
|
|
2230
|
+
return `
|
|
2231
|
+
<span class="studio-pinned-tabs" aria-label="Pinned preview tabs">
|
|
2232
|
+
<button type="button" role="tab" aria-selected="${state.studio.activeTab === "home" ? "true" : "false"}" class="studio-tab-button studio-tab-pinned studio-preview-tab${state.studio.activeTab === "home" ? " is-active" : ""}" data-action="studio-tab" data-tab="home" title="Home">
|
|
2233
|
+
<span class="dashicons dashicons-admin-home" aria-hidden="true"></span>
|
|
2234
|
+
<span>Home</span>
|
|
2235
|
+
${playgroundPort ? `<small>${escapeHtml(`:${playgroundPort}`)}</small>` : ""}
|
|
2236
|
+
</button>
|
|
2237
|
+
<button type="button" role="tab" aria-selected="${state.studio.activeTab === "admin" ? "true" : "false"}" class="studio-tab-button studio-tab-pinned studio-preview-tab${state.studio.activeTab === "admin" ? " is-active" : ""}" data-action="studio-tab" data-tab="admin" title="WP Admin">
|
|
2238
|
+
<span class="dashicons dashicons-admin-site-alt3" aria-hidden="true"></span>
|
|
2239
|
+
<span>WP Admin</span>
|
|
2240
|
+
${state.studio.playgroundUrl ? `<small>admin/password</small>` : ""}
|
|
2241
|
+
</button>
|
|
2242
|
+
</span>
|
|
2243
|
+
`;
|
|
2244
|
+
}
|
|
2245
|
+
|
|
2246
|
+
function renderStudioFileTabs() {
|
|
2247
|
+
const openFiles = studioOpenFileTabs();
|
|
2248
|
+
if (!openFiles.length) {
|
|
2249
|
+
return `<span class="studio-file-tabs-empty">Open a file from Explorer</span>`;
|
|
2250
|
+
}
|
|
2251
|
+
return `
|
|
2252
|
+
<span class="studio-file-tabs" aria-label="Open files">
|
|
2253
|
+
${openFiles.map((file) => renderStudioFileTab(file)).join("")}
|
|
2254
|
+
</span>
|
|
2255
|
+
`;
|
|
2256
|
+
}
|
|
2257
|
+
|
|
2258
|
+
function studioOpenFileTabs() {
|
|
2259
|
+
const knownFiles = new Map(state.studio.files.map((file) => [file.path, file]));
|
|
2260
|
+
const selectedPath = state.studio.selectedFile?.path;
|
|
2261
|
+
const paths = Array.isArray(state.studio.openFiles) ? [...state.studio.openFiles] : [];
|
|
2262
|
+
if (selectedPath && !paths.includes(selectedPath)) {
|
|
2263
|
+
paths.push(selectedPath);
|
|
2264
|
+
}
|
|
2265
|
+
state.studio.openFiles = paths;
|
|
2266
|
+
return paths.map((path) => knownFiles.get(path) ?? {
|
|
2267
|
+
path,
|
|
2268
|
+
name: path.split("/").pop() ?? path,
|
|
2269
|
+
directory: path.includes("/") ? path.split("/").slice(0, -1).join("/") : "",
|
|
2270
|
+
size: 0
|
|
2271
|
+
});
|
|
2272
|
+
}
|
|
2273
|
+
|
|
2274
|
+
function renderStudioFileTab(file) {
|
|
2275
|
+
const current = state.studio.activeTab === "editor" && file.path === state.studio.selectedFile?.path;
|
|
2276
|
+
const dirty = current && state.studio.dirty;
|
|
2277
|
+
return `
|
|
2278
|
+
<span class="studio-file-tab-wrap">
|
|
2279
|
+
<button type="button" role="tab" aria-selected="${current ? "true" : "false"}" class="studio-tab-button studio-editor-tab${current ? " is-active" : ""}" data-action="studio-file" data-path="${escapeAttr(file.path)}" title="${escapeAttr(file.path)}">
|
|
2280
|
+
<span class="dashicons ${studioFileIcon(file.path)}" aria-hidden="true"></span>
|
|
2281
|
+
<span>${escapeHtml(file.name ?? file.path)}</span>
|
|
2282
|
+
${dirty ? `<em aria-label="Unsaved changes"></em>` : ""}
|
|
2283
|
+
</button>
|
|
2284
|
+
<button type="button" class="studio-tab-close" data-action="studio-close-file-tab" data-path="${escapeAttr(file.path)}" aria-label="Close ${escapeAttr(file.name ?? file.path)}" title="Close">
|
|
2285
|
+
<span class="dashicons dashicons-no-alt" aria-hidden="true"></span>
|
|
2286
|
+
</button>
|
|
2287
|
+
</span>
|
|
2288
|
+
`;
|
|
2289
|
+
}
|
|
2290
|
+
|
|
2291
|
+
function scrollActiveStudioFileTabIntoView() {
|
|
2292
|
+
if (state.studio.activeTab !== "editor" || !state.studio.selectedFile?.path) {
|
|
2293
|
+
return;
|
|
2294
|
+
}
|
|
2295
|
+
requestAnimationFrame(() => {
|
|
2296
|
+
const container = document.querySelector(".studio-file-tabs");
|
|
2297
|
+
if (!container) {
|
|
2298
|
+
return;
|
|
2299
|
+
}
|
|
2300
|
+
const selectedPath = state.studio.selectedFile?.path;
|
|
2301
|
+
const activeTab = Array.from(container.querySelectorAll(".studio-tab-button[data-path]")).find(
|
|
2302
|
+
(button) => button.dataset.path === selectedPath
|
|
2303
|
+
);
|
|
2304
|
+
const target = activeTab?.closest(".studio-file-tab-wrap") ?? activeTab;
|
|
2305
|
+
if (!target) {
|
|
2306
|
+
return;
|
|
2307
|
+
}
|
|
2308
|
+
|
|
2309
|
+
const padding = 12;
|
|
2310
|
+
const containerRect = container.getBoundingClientRect();
|
|
2311
|
+
const targetRect = target.getBoundingClientRect();
|
|
2312
|
+
const targetLeft = targetRect.left - containerRect.left + container.scrollLeft;
|
|
2313
|
+
const targetRight = targetLeft + targetRect.width;
|
|
2314
|
+
const visibleLeft = container.scrollLeft;
|
|
2315
|
+
const visibleRight = visibleLeft + container.clientWidth;
|
|
2316
|
+
|
|
2317
|
+
if (targetLeft < visibleLeft + padding) {
|
|
2318
|
+
container.scrollLeft = Math.max(0, targetLeft - padding);
|
|
2319
|
+
} else if (targetRight > visibleRight - padding) {
|
|
2320
|
+
container.scrollLeft = targetRight - container.clientWidth + padding;
|
|
2321
|
+
}
|
|
2322
|
+
});
|
|
2323
|
+
}
|
|
2324
|
+
|
|
1726
2325
|
function renderStudioPlaygroundVersionModal() {
|
|
1727
2326
|
const modal = state.studio.playgroundVersionModal;
|
|
1728
2327
|
if (!modal) {
|
|
@@ -1879,6 +2478,21 @@ function renderStudioPlayButton() {
|
|
|
1879
2478
|
`;
|
|
1880
2479
|
}
|
|
1881
2480
|
|
|
2481
|
+
function renderStudioThemeToggle() {
|
|
2482
|
+
const theme = studioTheme();
|
|
2483
|
+
const isLight = theme === "light";
|
|
2484
|
+
const nextTheme = isLight ? "dark" : "light";
|
|
2485
|
+
return `
|
|
2486
|
+
<button class="studio-theme-switch" type="button" role="switch" aria-checked="${isLight ? "true" : "false"}" data-action="studio-toggle-theme" title="${escapeAttr(`Switch to ${nextTheme} mode`)}">
|
|
2487
|
+
<span class="dashicons dashicons-admin-appearance" aria-hidden="true"></span>
|
|
2488
|
+
<span class="studio-theme-switch-track" aria-hidden="true">
|
|
2489
|
+
<span></span>
|
|
2490
|
+
</span>
|
|
2491
|
+
<span class="studio-theme-switch-label">${escapeHtml(isLight ? "Light" : "Dark")}</span>
|
|
2492
|
+
</button>
|
|
2493
|
+
`;
|
|
2494
|
+
}
|
|
2495
|
+
|
|
1882
2496
|
function studioPreviewStateLabel() {
|
|
1883
2497
|
if (state.studio.running) {
|
|
1884
2498
|
return "Starting Playground";
|
|
@@ -1906,10 +2520,62 @@ function switchStudioTab(tab) {
|
|
|
1906
2520
|
if (!["editor", "home", "admin"].includes(tab)) {
|
|
1907
2521
|
return;
|
|
1908
2522
|
}
|
|
1909
|
-
|
|
2523
|
+
const discardingDirtyEditor =
|
|
2524
|
+
state.studio.activeTab === "editor" &&
|
|
2525
|
+
tab !== "editor" &&
|
|
2526
|
+
state.studio.dirty;
|
|
2527
|
+
if (discardingDirtyEditor) {
|
|
2528
|
+
if (!confirm("Discard unsaved changes in the current file?")) {
|
|
2529
|
+
return;
|
|
2530
|
+
}
|
|
2531
|
+
state.studio.draftContent = state.studio.fileContent;
|
|
2532
|
+
state.studio.dirty = false;
|
|
2533
|
+
} else {
|
|
2534
|
+
captureStudioEditorValue();
|
|
2535
|
+
}
|
|
1910
2536
|
state.studio.activeTab = tab;
|
|
1911
2537
|
renderStudio();
|
|
1912
2538
|
remountStudioEditorIfNeeded();
|
|
2539
|
+
updateRouteFromState();
|
|
2540
|
+
}
|
|
2541
|
+
|
|
2542
|
+
async function closeStudioFileTab(relativePath) {
|
|
2543
|
+
if (!relativePath) {
|
|
2544
|
+
return;
|
|
2545
|
+
}
|
|
2546
|
+
const openFiles = Array.isArray(state.studio.openFiles) ? state.studio.openFiles : [];
|
|
2547
|
+
const index = openFiles.indexOf(relativePath);
|
|
2548
|
+
if (index === -1) {
|
|
2549
|
+
return;
|
|
2550
|
+
}
|
|
2551
|
+
const isCurrent = relativePath === state.studio.selectedFile?.path;
|
|
2552
|
+
if (isCurrent && state.studio.dirty && !confirm("Discard unsaved changes in the current file?")) {
|
|
2553
|
+
return;
|
|
2554
|
+
}
|
|
2555
|
+
|
|
2556
|
+
captureStudioEditorValue();
|
|
2557
|
+
const nextOpenFiles = openFiles.filter((path) => path !== relativePath);
|
|
2558
|
+
state.studio.openFiles = nextOpenFiles;
|
|
2559
|
+
|
|
2560
|
+
if (!isCurrent) {
|
|
2561
|
+
renderStudio();
|
|
2562
|
+
remountStudioEditorIfNeeded();
|
|
2563
|
+
return;
|
|
2564
|
+
}
|
|
2565
|
+
|
|
2566
|
+
state.studio.dirty = false;
|
|
2567
|
+
const nextPath = nextOpenFiles[Math.min(index, nextOpenFiles.length - 1)];
|
|
2568
|
+
if (nextPath) {
|
|
2569
|
+
await selectStudioFile(nextPath, { force: true });
|
|
2570
|
+
return;
|
|
2571
|
+
}
|
|
2572
|
+
|
|
2573
|
+
state.studio.selectedFile = null;
|
|
2574
|
+
state.studio.fileContent = "";
|
|
2575
|
+
state.studio.draftContent = "";
|
|
2576
|
+
state.studio.activeTab = "home";
|
|
2577
|
+
renderStudio();
|
|
2578
|
+
updateRouteFromState();
|
|
1913
2579
|
}
|
|
1914
2580
|
|
|
1915
2581
|
function toggleStudioTerminal() {
|
|
@@ -1919,6 +2585,39 @@ function toggleStudioTerminal() {
|
|
|
1919
2585
|
remountStudioEditorIfNeeded();
|
|
1920
2586
|
}
|
|
1921
2587
|
|
|
2588
|
+
function toggleStudioPanel(panel) {
|
|
2589
|
+
if (!["files", "sidebar"].includes(panel)) {
|
|
2590
|
+
return;
|
|
2591
|
+
}
|
|
2592
|
+
captureStudioEditorValue();
|
|
2593
|
+
state.studio.panels = normalizeStudioPanelState(state.studio.panels);
|
|
2594
|
+
state.studio.panels[panel] = !state.studio.panels[panel];
|
|
2595
|
+
saveStudioPanelState(state.studio.panels);
|
|
2596
|
+
renderStudio();
|
|
2597
|
+
remountStudioEditorIfNeeded();
|
|
2598
|
+
requestAnimationFrame(() => state.studio.editor?.layout?.());
|
|
2599
|
+
}
|
|
2600
|
+
|
|
2601
|
+
function openStudioSidebarTab(tab) {
|
|
2602
|
+
captureStudioEditorValue();
|
|
2603
|
+
state.studio.panels = normalizeStudioPanelState(state.studio.panels);
|
|
2604
|
+
state.studio.panels.sidebar = true;
|
|
2605
|
+
saveStudioPanelState(state.studio.panels);
|
|
2606
|
+
setStudioSidebarTab(tab);
|
|
2607
|
+
renderStudio();
|
|
2608
|
+
remountStudioEditorIfNeeded();
|
|
2609
|
+
requestAnimationFrame(() => state.studio.editor?.layout?.());
|
|
2610
|
+
}
|
|
2611
|
+
|
|
2612
|
+
function toggleStudioTheme() {
|
|
2613
|
+
captureStudioEditorValue();
|
|
2614
|
+
state.studio.theme = studioTheme() === "light" ? "dark" : "light";
|
|
2615
|
+
saveStudioTheme(state.studio.theme);
|
|
2616
|
+
renderStudio();
|
|
2617
|
+
remountStudioEditorIfNeeded();
|
|
2618
|
+
requestAnimationFrame(() => state.studio.editor?.layout?.());
|
|
2619
|
+
}
|
|
2620
|
+
|
|
1922
2621
|
function toggleStudioFolder(folderPath) {
|
|
1923
2622
|
if (!folderPath) {
|
|
1924
2623
|
return;
|
|
@@ -2027,6 +2726,12 @@ function renderStudioAiSidebar() {
|
|
|
2027
2726
|
|
|
2028
2727
|
function renderStudioSidebarTabs(activeTab) {
|
|
2029
2728
|
return `
|
|
2729
|
+
<header class="studio-secondary-header">
|
|
2730
|
+
<strong>${activeTab === "release" ? "Release" : "AI Helper"}</strong>
|
|
2731
|
+
<button class="studio-pane-action" type="button" data-action="studio-toggle-sidebar" aria-label="Hide Secondary Side Bar" title="Hide Secondary Side Bar">
|
|
2732
|
+
<span class="dashicons dashicons-no-alt" aria-hidden="true"></span>
|
|
2733
|
+
</button>
|
|
2734
|
+
</header>
|
|
2030
2735
|
<div class="studio-sidebar-tabs ps-segmented" role="tablist" aria-label="Studio sidebar">
|
|
2031
2736
|
<button class="ps-segmented-option${activeTab === "ai" ? " is-active" : ""}" type="button" role="tab" aria-selected="${activeTab === "ai"}" data-action="studio-sidebar-tab" data-tab="ai">
|
|
2032
2737
|
<span class="dashicons dashicons-format-chat" aria-hidden="true"></span>
|
|
@@ -2245,7 +2950,7 @@ function renderStudioAiMessages() {
|
|
|
2245
2950
|
<span>${escapeHtml(aiMessageRoleLabel(message))}</span>
|
|
2246
2951
|
<time>${escapeHtml(formatTime(message.createdAt))}</time>
|
|
2247
2952
|
</header>
|
|
2248
|
-
<
|
|
2953
|
+
<div class="studio-ai-markdown">${renderStudioAiMarkdown(message.text)}</div>
|
|
2249
2954
|
</div>
|
|
2250
2955
|
</article>
|
|
2251
2956
|
`
|
|
@@ -2711,7 +3416,7 @@ function appendStudioAiOutput(text, tone = "log") {
|
|
|
2711
3416
|
}
|
|
2712
3417
|
last.text = `${last.text}${output}`;
|
|
2713
3418
|
last.tone = tone === "error" ? "error" : last.tone === "status" ? "log" : last.tone;
|
|
2714
|
-
|
|
3419
|
+
updateStudioAiMessageList();
|
|
2715
3420
|
}
|
|
2716
3421
|
|
|
2717
3422
|
function updateStudioAiSidebar() {
|
|
@@ -2723,11 +3428,40 @@ function updateStudioAiSidebar() {
|
|
|
2723
3428
|
node.innerHTML = renderStudioAiSidebar();
|
|
2724
3429
|
const messages = document.getElementById("studio-ai-messages");
|
|
2725
3430
|
if (messages) {
|
|
2726
|
-
messages
|
|
3431
|
+
scrollStudioAiMessagesToBottom(messages);
|
|
2727
3432
|
}
|
|
2728
3433
|
updateStudioAiControls();
|
|
2729
3434
|
}
|
|
2730
3435
|
|
|
3436
|
+
function updateStudioAiMessageList(options = {}) {
|
|
3437
|
+
const messages = document.getElementById("studio-ai-messages");
|
|
3438
|
+
if (!messages) {
|
|
3439
|
+
updateStudioAiSidebar();
|
|
3440
|
+
return;
|
|
3441
|
+
}
|
|
3442
|
+
|
|
3443
|
+
const shouldStick = options.forceScroll || isStudioAiMessagesNearBottom(messages);
|
|
3444
|
+
messages.innerHTML = renderStudioAiMessages();
|
|
3445
|
+
if (shouldStick) {
|
|
3446
|
+
scrollStudioAiMessagesToBottom(messages);
|
|
3447
|
+
}
|
|
3448
|
+
updateStudioAiControls();
|
|
3449
|
+
}
|
|
3450
|
+
|
|
3451
|
+
function isStudioAiMessagesNearBottom(messages) {
|
|
3452
|
+
return messages.scrollHeight - messages.scrollTop - messages.clientHeight < 80;
|
|
3453
|
+
}
|
|
3454
|
+
|
|
3455
|
+
function scrollStudioAiMessagesToBottom(messages) {
|
|
3456
|
+
const previousScrollBehavior = messages.style.scrollBehavior;
|
|
3457
|
+
messages.style.scrollBehavior = "auto";
|
|
3458
|
+
messages.scrollTop = messages.scrollHeight;
|
|
3459
|
+
requestAnimationFrame(() => {
|
|
3460
|
+
messages.scrollTop = messages.scrollHeight;
|
|
3461
|
+
messages.style.scrollBehavior = previousScrollBehavior;
|
|
3462
|
+
});
|
|
3463
|
+
}
|
|
3464
|
+
|
|
2731
3465
|
function updateStudioAiControls() {
|
|
2732
3466
|
const prompt = document.getElementById("studio-ai-prompt");
|
|
2733
3467
|
const send = document.getElementById("studio-ai-send-button");
|
|
@@ -3078,7 +3812,7 @@ async function mountStudioEditor(content) {
|
|
|
3078
3812
|
);
|
|
3079
3813
|
state.studio.editorModels = [originalModel, modifiedModel];
|
|
3080
3814
|
state.studio.editor = monaco.editor.createDiffEditor(container, {
|
|
3081
|
-
theme:
|
|
3815
|
+
theme: studioMonacoTheme(),
|
|
3082
3816
|
readOnly: true,
|
|
3083
3817
|
automaticLayout: true,
|
|
3084
3818
|
minimap: { enabled: false },
|
|
@@ -3101,7 +3835,7 @@ async function mountStudioEditor(content) {
|
|
|
3101
3835
|
state.studio.editor = monaco.editor.create(container, {
|
|
3102
3836
|
value: content,
|
|
3103
3837
|
language: languageForPath(state.studio.selectedFile?.path ?? ""),
|
|
3104
|
-
theme:
|
|
3838
|
+
theme: studioMonacoTheme(),
|
|
3105
3839
|
readOnly: state.studio.readOnly,
|
|
3106
3840
|
automaticLayout: true,
|
|
3107
3841
|
minimap: { enabled: false },
|
|
@@ -4784,36 +5518,35 @@ function configureAutoRefresh() {
|
|
|
4784
5518
|
* View switching with view-transitions
|
|
4785
5519
|
* =================================================================== */
|
|
4786
5520
|
|
|
4787
|
-
function showView(view) {
|
|
4788
|
-
|
|
4789
|
-
|
|
5521
|
+
function showView(view, options = {}) {
|
|
5522
|
+
const nextView = normalizeViewId(view);
|
|
5523
|
+
if (state.activeView === nextView) {
|
|
5524
|
+
if (options.updateRoute !== false) {
|
|
5525
|
+
updateRouteFromState({ replace: options.replaceRoute });
|
|
5526
|
+
}
|
|
5527
|
+
return Promise.resolve();
|
|
4790
5528
|
}
|
|
4791
5529
|
const apply = () => {
|
|
4792
|
-
|
|
4793
|
-
document.body.dataset.activeView = view;
|
|
4794
|
-
document.querySelectorAll(".view").forEach((node) => node.classList.remove("is-active"));
|
|
4795
|
-
document.getElementById(`view-${view}`)?.classList.add("is-active");
|
|
4796
|
-
document
|
|
4797
|
-
.querySelectorAll("#adminmenu li")
|
|
4798
|
-
.forEach((node) => node.classList.remove("wp-has-current-submenu"));
|
|
4799
|
-
document
|
|
4800
|
-
.querySelector(`#adminmenu li[data-view="${view}"]`)
|
|
4801
|
-
?.classList.add("wp-has-current-submenu");
|
|
5530
|
+
applyActiveViewShell(nextView);
|
|
4802
5531
|
closeDetail();
|
|
4803
|
-
if (
|
|
5532
|
+
if (nextView === "release") {
|
|
4804
5533
|
if (!state.releaseBoard.loading && !state.releaseBoard.plugins.length) {
|
|
4805
5534
|
void loadReleaseBoard();
|
|
4806
5535
|
} else {
|
|
4807
5536
|
renderReleaseBoard();
|
|
4808
5537
|
}
|
|
4809
5538
|
}
|
|
5539
|
+
if (options.updateRoute !== false) {
|
|
5540
|
+
updateRouteFromState({ replace: options.replaceRoute });
|
|
5541
|
+
}
|
|
4810
5542
|
};
|
|
4811
5543
|
|
|
4812
5544
|
if (typeof document.startViewTransition === "function") {
|
|
4813
|
-
document.startViewTransition(apply);
|
|
4814
|
-
|
|
4815
|
-
apply();
|
|
5545
|
+
const transition = document.startViewTransition(apply);
|
|
5546
|
+
return transition.updateCallbackDone?.catch(() => {}) ?? Promise.resolve();
|
|
4816
5547
|
}
|
|
5548
|
+
apply();
|
|
5549
|
+
return Promise.resolve();
|
|
4817
5550
|
}
|
|
4818
5551
|
|
|
4819
5552
|
/* ===================================================================
|
|
@@ -5215,6 +5948,16 @@ function renderStudioAiMarkdown(value) {
|
|
|
5215
5948
|
}
|
|
5216
5949
|
}
|
|
5217
5950
|
|
|
5951
|
+
function refreshStudioAiMarkdownIfReady() {
|
|
5952
|
+
try {
|
|
5953
|
+
if ((state.studio.aiMessages ?? []).some((message) => message.role === "assistant")) {
|
|
5954
|
+
updateStudioAiMessageList();
|
|
5955
|
+
}
|
|
5956
|
+
} catch {
|
|
5957
|
+
// The markdown parser can finish loading before Studio state exists.
|
|
5958
|
+
}
|
|
5959
|
+
}
|
|
5960
|
+
|
|
5218
5961
|
function basicMarkdownToHtml(value) {
|
|
5219
5962
|
const lines = String(value ?? "").replace(/\r\n?/g, "\n").split("\n");
|
|
5220
5963
|
const blocks = [];
|
|
@@ -5649,9 +6392,6 @@ function studioPluginKey() {
|
|
|
5649
6392
|
|
|
5650
6393
|
function setStudioSidebarTab(tab) {
|
|
5651
6394
|
const next = tab === "release" ? "release" : "ai";
|
|
5652
|
-
if (state.studio.sidebarTab === next) {
|
|
5653
|
-
return;
|
|
5654
|
-
}
|
|
5655
6395
|
state.studio.sidebarTab = next;
|
|
5656
6396
|
saveStudioSidebarTab(studioPluginKey(), next);
|
|
5657
6397
|
updateStudioSidebar();
|
|
@@ -5715,7 +6455,7 @@ function renderStudioReleasePane() {
|
|
|
5715
6455
|
</header>
|
|
5716
6456
|
<ol class="ps-release-funnel">
|
|
5717
6457
|
${renderStudioReleaseStepVersion(versionState, release)}
|
|
5718
|
-
${renderStudioReleaseStepTags(release)}
|
|
6458
|
+
${renderStudioReleaseStepTags(versionState, release)}
|
|
5719
6459
|
${renderStudioReleaseStepValidate(versionState, release)}
|
|
5720
6460
|
${renderStudioReleaseStepPublish(versionState, release)}
|
|
5721
6461
|
</ol>
|
|
@@ -5793,7 +6533,11 @@ function renderStudioReleaseStepVersion(versionState, release) {
|
|
|
5793
6533
|
return renderStudioReleaseStepShell(1, "Version state", summary, body);
|
|
5794
6534
|
}
|
|
5795
6535
|
|
|
5796
|
-
function
|
|
6536
|
+
function releaseTagDraftValue(versionState, release) {
|
|
6537
|
+
return release.newTagDraft || versionState?.localVersion || "";
|
|
6538
|
+
}
|
|
6539
|
+
|
|
6540
|
+
function renderStudioReleaseStepTags(versionState, release) {
|
|
5797
6541
|
let body;
|
|
5798
6542
|
if (release.tagsLoading && !release.tags) {
|
|
5799
6543
|
body = loadingShell("Reading SVN tags…");
|
|
@@ -5814,22 +6558,31 @@ function renderStudioReleaseStepTags(release) {
|
|
|
5814
6558
|
const tagRows = (list.tags ?? [])
|
|
5815
6559
|
.map((tag) => renderStudioReleaseTagRow(tag))
|
|
5816
6560
|
.join("");
|
|
6561
|
+
const newTagValue = releaseTagDraftValue(versionState, release);
|
|
6562
|
+
const currentVersionTag = versionState?.localVersion
|
|
6563
|
+
? (list.tags ?? []).find((tag) => tag.name === versionState.localVersion)
|
|
6564
|
+
: null;
|
|
6565
|
+
const newTagControl = currentVersionTag
|
|
6566
|
+
? currentVersionTag.isUncommitted
|
|
6567
|
+
? `<p class="ps-release-tag-ready"><span class="dashicons dashicons-yes-alt" aria-hidden="true"></span>${escapeHtml(`Tag ${versionState.localVersion} is ready. Run a dry-run release next.`)}</p>`
|
|
6568
|
+
: `<p class="ps-release-tag-ready is-blocked"><span class="dashicons dashicons-warning" aria-hidden="true"></span>${escapeHtml(`Tag ${versionState.localVersion} already exists on WordPress.org SVN. Bump the version before creating a new tag.`)}</p>`
|
|
6569
|
+
: `<div class="ps-release-step-newtag">
|
|
6570
|
+
<label>
|
|
6571
|
+
<span>Create tag from current version</span>
|
|
6572
|
+
<input type="text" id="studio-release-new-tag" value="${escapeAttr(newTagValue)}" placeholder="1.2.3" />
|
|
6573
|
+
</label>
|
|
6574
|
+
<button class="button button-secondary" type="button" data-action="studio-release-create">
|
|
6575
|
+
<span class="dashicons dashicons-plus-alt2" aria-hidden="true"></span>
|
|
6576
|
+
Create tag
|
|
6577
|
+
</button>
|
|
6578
|
+
</div>`;
|
|
5817
6579
|
body = `
|
|
5818
6580
|
<ul class="ps-release-tag-list">
|
|
5819
6581
|
${trunkRow}
|
|
5820
6582
|
${tagRows || `<li class="ps-release-tag-empty">No tags yet.</li>`}
|
|
5821
6583
|
</ul>
|
|
5822
6584
|
${renderStudioReleaseSwitchConflict(release)}
|
|
5823
|
-
|
|
5824
|
-
<label>
|
|
5825
|
-
<span>Create tag from current version</span>
|
|
5826
|
-
<input type="text" id="studio-release-new-tag" value="${escapeAttr(release.newTagDraft ?? "")}" placeholder="1.2.3" />
|
|
5827
|
-
</label>
|
|
5828
|
-
<button class="button button-secondary" type="button" data-action="studio-release-create">
|
|
5829
|
-
<span class="dashicons dashicons-plus-alt2" aria-hidden="true"></span>
|
|
5830
|
-
Create tag
|
|
5831
|
-
</button>
|
|
5832
|
-
</div>
|
|
6585
|
+
${newTagControl}
|
|
5833
6586
|
${release.newTagError ? `<p class="ps-release-inline-error"><span class="dashicons dashicons-warning" aria-hidden="true"></span>${escapeHtml(release.newTagError)}</p>` : ""}
|
|
5834
6587
|
`;
|
|
5835
6588
|
}
|
|
@@ -5881,9 +6634,11 @@ function renderStudioReleaseTagRow(tag) {
|
|
|
5881
6634
|
const switchingLabel = release.switchingResolution === "override" || release.switchingResolution === "revert"
|
|
5882
6635
|
? "Resolving"
|
|
5883
6636
|
: "Switching";
|
|
5884
|
-
const switchButton = tag.
|
|
5885
|
-
? ""
|
|
5886
|
-
:
|
|
6637
|
+
const switchButton = tag.isUncommitted
|
|
6638
|
+
? `<span class="ps-release-tag-local-note" title="This tag exists locally and will be published by the release step.">Ready for release</span>`
|
|
6639
|
+
: tag.isCurrent
|
|
6640
|
+
? ""
|
|
6641
|
+
: `<button class="button button-small" type="button" data-action="studio-release-switch" data-tag="${escapeAttr(tag.name)}" ${anySwitching ? "disabled aria-disabled=\"true\"" : ""}>${switching ? `<span class="dashicons dashicons-update" aria-hidden="true"></span>${switchingLabel}` : "Switch"}</button>`;
|
|
5887
6642
|
const confirmKey = `delete-tag:${tag.name}`;
|
|
5888
6643
|
const pendingConfirm = state.studio.pendingConfirms?.get(confirmKey);
|
|
5889
6644
|
const deleteButton = tag.isUncommitted
|
|
@@ -5939,6 +6694,117 @@ function renderStudioReleaseStepValidate(versionState, release) {
|
|
|
5939
6694
|
return renderStudioReleaseStepShell(3, "Validate", summaryLine, body);
|
|
5940
6695
|
}
|
|
5941
6696
|
|
|
6697
|
+
function studioPublishRouteDetails(action) {
|
|
6698
|
+
switch (action) {
|
|
6699
|
+
case "submit":
|
|
6700
|
+
return {
|
|
6701
|
+
label: "Submit new plugin",
|
|
6702
|
+
shortLabel: "submit",
|
|
6703
|
+
icon: "dashicons-upload",
|
|
6704
|
+
description: "Use this for a first WordPress.org review. Pressship builds a package and uploads it to the plugin submission flow after confirmation.",
|
|
6705
|
+
dryRunLabel: "Dry-run submit",
|
|
6706
|
+
confirmLabel: "Confirm submit",
|
|
6707
|
+
resultLabel: "WordPress.org submission"
|
|
6708
|
+
};
|
|
6709
|
+
case "release":
|
|
6710
|
+
return {
|
|
6711
|
+
label: "Release update",
|
|
6712
|
+
shortLabel: "release",
|
|
6713
|
+
icon: "dashicons-update",
|
|
6714
|
+
description: "Use this for an approved plugin that already has an SVN repository. Pressship validates the package and publishes the current version after confirmation.",
|
|
6715
|
+
dryRunLabel: "Dry-run release",
|
|
6716
|
+
confirmLabel: "Confirm release",
|
|
6717
|
+
resultLabel: "SVN release"
|
|
6718
|
+
};
|
|
6719
|
+
default:
|
|
6720
|
+
return {
|
|
6721
|
+
label: "Auto decide",
|
|
6722
|
+
shortLabel: "auto",
|
|
6723
|
+
icon: "dashicons-controls-play",
|
|
6724
|
+
description: "Best default. Pressship checks WordPress.org and SVN, then chooses submit for first-time review or release for an existing plugin.",
|
|
6725
|
+
dryRunLabel: "Dry-run auto",
|
|
6726
|
+
confirmLabel: "Confirm publish",
|
|
6727
|
+
resultLabel: "publish route"
|
|
6728
|
+
};
|
|
6729
|
+
}
|
|
6730
|
+
}
|
|
6731
|
+
|
|
6732
|
+
function renderStudioReleasePublishOption(action, options = {}) {
|
|
6733
|
+
const details = studioPublishRouteDetails(action);
|
|
6734
|
+
const active = options.activeAction === action;
|
|
6735
|
+
const chosen = options.detectedRoute === action;
|
|
6736
|
+
const isDefault = options.defaultAction === action;
|
|
6737
|
+
const recommended = action === "auto";
|
|
6738
|
+
const disabled = options.running ? "disabled aria-disabled=\"true\"" : "";
|
|
6739
|
+
const badges = [
|
|
6740
|
+
recommended ? "Recommended" : "",
|
|
6741
|
+
isDefault ? "Default" : "",
|
|
6742
|
+
chosen ? "Selected" : ""
|
|
6743
|
+
].filter(Boolean);
|
|
6744
|
+
|
|
6745
|
+
return `
|
|
6746
|
+
<button class="ps-release-publish-option${active ? " is-active" : ""}${chosen ? " is-selected" : ""}" type="button" data-action="dry-run-publish" data-id="${escapeAttr(state.studio.id)}" data-publish-action="${escapeAttr(action)}" ${disabled}>
|
|
6747
|
+
<span class="ps-release-option-icon dashicons ${escapeAttr(details.icon)}" aria-hidden="true"></span>
|
|
6748
|
+
<span class="ps-release-option-content">
|
|
6749
|
+
<span class="ps-release-option-title">
|
|
6750
|
+
<strong>${escapeHtml(details.label)}</strong>
|
|
6751
|
+
${badges.map((badge) => `<em>${escapeHtml(badge)}</em>`).join("")}
|
|
6752
|
+
</span>
|
|
6753
|
+
<span class="ps-release-option-copy">${escapeHtml(details.description)}</span>
|
|
6754
|
+
</span>
|
|
6755
|
+
<span class="ps-release-option-action">${escapeHtml(details.dryRunLabel)}</span>
|
|
6756
|
+
</button>
|
|
6757
|
+
`;
|
|
6758
|
+
}
|
|
6759
|
+
|
|
6760
|
+
function renderStudioReleasePublishSummary(dryRun, pendingConfirm, running) {
|
|
6761
|
+
if (running) {
|
|
6762
|
+
return `
|
|
6763
|
+
<div class="ps-release-publish-summary is-running" role="status">
|
|
6764
|
+
<span class="dashicons dashicons-update" aria-hidden="true"></span>
|
|
6765
|
+
<p>
|
|
6766
|
+
<strong>Previewing publish route</strong>
|
|
6767
|
+
<small>Validating the package and checking which path is safe to confirm.</small>
|
|
6768
|
+
</p>
|
|
6769
|
+
</div>
|
|
6770
|
+
`;
|
|
6771
|
+
}
|
|
6772
|
+
|
|
6773
|
+
if (!dryRun) {
|
|
6774
|
+
return `
|
|
6775
|
+
<p class="ps-release-step-muted">
|
|
6776
|
+
Pick a dry-run path above. A dry-run validates the package and shows exactly what will happen; nothing is uploaded or committed until you confirm.
|
|
6777
|
+
</p>
|
|
6778
|
+
`;
|
|
6779
|
+
}
|
|
6780
|
+
|
|
6781
|
+
const routeAction = dryRun.route?.action ?? "publish";
|
|
6782
|
+
const details = studioPublishRouteDetails(routeAction);
|
|
6783
|
+
const packageSummary = dryRun.package?.fileCount
|
|
6784
|
+
? `${dryRun.package.fileCount} packaged files`
|
|
6785
|
+
: dryRun.package?.topLevelFolder
|
|
6786
|
+
? `Package folder: ${dryRun.package.topLevelFolder}`
|
|
6787
|
+
: "";
|
|
6788
|
+
|
|
6789
|
+
return `
|
|
6790
|
+
<div class="ps-release-publish-summary">
|
|
6791
|
+
<div class="ps-release-publish-result">
|
|
6792
|
+
<span class="dashicons ${escapeAttr(details.icon)}" aria-hidden="true"></span>
|
|
6793
|
+
<p>
|
|
6794
|
+
<strong>${escapeHtml(details.label)}</strong>
|
|
6795
|
+
<small>${escapeHtml(dryRun.route?.reason ?? `Ready to preview ${details.resultLabel}.`)}</small>
|
|
6796
|
+
${packageSummary ? `<small>${escapeHtml(packageSummary)}</small>` : ""}
|
|
6797
|
+
</p>
|
|
6798
|
+
</div>
|
|
6799
|
+
${dryRun.canConfirm && dryRun.approvalId
|
|
6800
|
+
? `<button class="button button-primary ps-release-confirm-button${pendingConfirm ? " is-confirming" : ""}" type="button" data-action="studio-release-publish" data-approval-id="${escapeAttr(dryRun.approvalId)}" data-action-label="${escapeAttr(routeAction)}">
|
|
6801
|
+
${pendingConfirm ? "Click again to confirm" : escapeHtml(details.confirmLabel)}
|
|
6802
|
+
</button>`
|
|
6803
|
+
: `<p class="ps-release-step-muted">Dry-run did not pass. Fix validation or version findings before publishing.</p>`}
|
|
6804
|
+
</div>
|
|
6805
|
+
`;
|
|
6806
|
+
}
|
|
6807
|
+
|
|
5942
6808
|
function renderStudioReleaseStepPublish(versionState, release) {
|
|
5943
6809
|
const defaultAction = state.settings?.defaultPublishAction ?? "auto";
|
|
5944
6810
|
const dryRun = release.dryRun;
|
|
@@ -5946,39 +6812,23 @@ function renderStudioReleaseStepPublish(versionState, release) {
|
|
|
5946
6812
|
const publishConfirmKey = "publish";
|
|
5947
6813
|
const pendingConfirm = state.studio.pendingConfirms?.get(publishConfirmKey);
|
|
5948
6814
|
const detectedRoute = dryRun?.route?.action;
|
|
6815
|
+
const activeAction = detectedRoute ?? defaultAction;
|
|
5949
6816
|
|
|
5950
6817
|
const body = `
|
|
6818
|
+
<div class="ps-release-publish-guide">
|
|
6819
|
+
<strong>Dry-run first, confirm second.</strong>
|
|
6820
|
+
<span>Choose the route you want to preview. The dry-run is safe; the later confirm button performs the upload or SVN publish.</span>
|
|
6821
|
+
</div>
|
|
5951
6822
|
<div class="ps-release-publish-options">
|
|
5952
|
-
|
|
5953
|
-
|
|
5954
|
-
|
|
5955
|
-
</button>
|
|
5956
|
-
<button class="button${detectedRoute === "release" ? " button-primary" : ""}" type="button" data-action="dry-run-publish" data-id="${escapeAttr(state.studio.id)}" data-publish-action="release" ${running ? "disabled" : ""}>
|
|
5957
|
-
<span class="dashicons dashicons-update" aria-hidden="true"></span>
|
|
5958
|
-
Dry-run release
|
|
5959
|
-
</button>
|
|
5960
|
-
<button class="button${defaultAction === "auto" ? " button-primary" : ""}" type="button" data-action="dry-run-publish" data-id="${escapeAttr(state.studio.id)}" data-publish-action="auto" ${running ? "disabled" : ""}>
|
|
5961
|
-
<span class="dashicons dashicons-controls-play" aria-hidden="true"></span>
|
|
5962
|
-
Auto dry-run
|
|
5963
|
-
</button>
|
|
6823
|
+
${renderStudioReleasePublishOption("auto", { activeAction, defaultAction, detectedRoute, running })}
|
|
6824
|
+
${renderStudioReleasePublishOption("submit", { activeAction, defaultAction, detectedRoute, running })}
|
|
6825
|
+
${renderStudioReleasePublishOption("release", { activeAction, defaultAction, detectedRoute, running })}
|
|
5964
6826
|
</div>
|
|
5965
|
-
${dryRun
|
|
5966
|
-
? `<div class="ps-release-publish-summary">
|
|
5967
|
-
<p>
|
|
5968
|
-
<strong>${escapeHtml(dryRun.route?.action ?? "publish")}</strong>
|
|
5969
|
-
<small>${escapeHtml(dryRun.route?.reason ?? "")}</small>
|
|
5970
|
-
</p>
|
|
5971
|
-
${dryRun.canConfirm && dryRun.approvalId
|
|
5972
|
-
? `<button class="button button-primary ps-release-confirm-button${pendingConfirm ? " is-confirming" : ""}" type="button" data-action="studio-release-publish" data-approval-id="${escapeAttr(dryRun.approvalId)}" data-action-label="${escapeAttr(dryRun.route?.action ?? "publish")}">
|
|
5973
|
-
${pendingConfirm ? "Click again to confirm" : `Confirm ${escapeHtml(dryRun.route?.action ?? "publish")}`}
|
|
5974
|
-
</button>`
|
|
5975
|
-
: `<p class="ps-release-step-muted">Dry-run did not pass — fix validation findings before publishing.</p>`}
|
|
5976
|
-
</div>`
|
|
5977
|
-
: `<p class="ps-release-step-muted">Run a dry-run to preview the publish plan.</p>`}
|
|
6827
|
+
${renderStudioReleasePublishSummary(dryRun, pendingConfirm, running)}
|
|
5978
6828
|
`;
|
|
5979
6829
|
|
|
5980
6830
|
const summary = dryRun
|
|
5981
|
-
? `Detected route: ${escapeHtml(dryRun.route?.action
|
|
6831
|
+
? `Detected route: ${escapeHtml(studioPublishRouteDetails(dryRun.route?.action).shortLabel)}`
|
|
5982
6832
|
: versionState?.releaseBlocked
|
|
5983
6833
|
? "Blocked — fix version state before publishing"
|
|
5984
6834
|
: "";
|
|
@@ -6060,10 +6910,51 @@ async function refreshStudioAfterReleaseSwitch(result = {}) {
|
|
|
6060
6910
|
}
|
|
6061
6911
|
}
|
|
6062
6912
|
|
|
6913
|
+
async function refreshStudioAfterVersionChange(localId) {
|
|
6914
|
+
if (!localId || state.studio.id !== localId || state.studio.scope !== "local") {
|
|
6915
|
+
return;
|
|
6916
|
+
}
|
|
6917
|
+
|
|
6918
|
+
const selectedPath = state.studio.selectedFile?.path;
|
|
6919
|
+
try {
|
|
6920
|
+
const [detail, filesResult, checkState, versionState] = await Promise.all([
|
|
6921
|
+
api(`/api/plugins/local/${encodeURIComponent(localId)}`),
|
|
6922
|
+
api(`/api/plugins/local/${encodeURIComponent(localId)}/files`),
|
|
6923
|
+
api(`/api/plugins/local/${encodeURIComponent(localId)}/check-state`).catch(() => ({ state: null })),
|
|
6924
|
+
api(`/api/plugins/local/${encodeURIComponent(localId)}/version-state`).catch(() => null)
|
|
6925
|
+
]);
|
|
6926
|
+
|
|
6927
|
+
applyStudioPluginDetail("local", localId, detail);
|
|
6928
|
+
state.studio.files = filesResult.files ?? [];
|
|
6929
|
+
applyStudioCheckState(checkState.state);
|
|
6930
|
+
if (versionState) {
|
|
6931
|
+
state.versionStates.set(localId, versionState);
|
|
6932
|
+
if (!state.studio.release.newTagDraft || state.studio.release.newTagDraft === versionState.latestSvnTag) {
|
|
6933
|
+
state.studio.release.newTagDraft = versionState.localVersion ?? "";
|
|
6934
|
+
}
|
|
6935
|
+
}
|
|
6936
|
+
|
|
6937
|
+
const selectedStillExists = selectedPath && state.studio.files.some((file) => file.path === selectedPath);
|
|
6938
|
+
if (selectedStillExists) {
|
|
6939
|
+
await selectStudioFile(selectedPath, { force: true });
|
|
6940
|
+
} else {
|
|
6941
|
+
renderStudio();
|
|
6942
|
+
remountStudioEditorIfNeeded();
|
|
6943
|
+
}
|
|
6944
|
+
updateStudioSidebar();
|
|
6945
|
+
updateStudioControls();
|
|
6946
|
+
} catch (error) {
|
|
6947
|
+
appendStudioTerminal(`Studio reload after version change failed: ${error.message}`, "error");
|
|
6948
|
+
renderStudio();
|
|
6949
|
+
updateStudioSidebar();
|
|
6950
|
+
}
|
|
6951
|
+
}
|
|
6952
|
+
|
|
6063
6953
|
async function createStudioReleaseTag() {
|
|
6064
6954
|
if (!state.studio.id) return;
|
|
6065
6955
|
const input = document.getElementById("studio-release-new-tag");
|
|
6066
|
-
const
|
|
6956
|
+
const versionState = state.versionStates.get(state.studio.id);
|
|
6957
|
+
const name = (input?.value ?? releaseTagDraftValue(versionState, state.studio.release)).trim();
|
|
6067
6958
|
if (!name) {
|
|
6068
6959
|
state.studio.release.newTagError = "Enter a tag name first.";
|
|
6069
6960
|
updateStudioSidebar();
|
|
@@ -6174,7 +7065,7 @@ async function bumpStudioReleaseVersion(localId, bump) {
|
|
|
6174
7065
|
}, 1500);
|
|
6175
7066
|
await loadLocal();
|
|
6176
7067
|
if (state.studio.id === localId) {
|
|
6177
|
-
await
|
|
7068
|
+
await refreshStudioAfterVersionChange(localId);
|
|
6178
7069
|
}
|
|
6179
7070
|
} catch (error) {
|
|
6180
7071
|
state.studio.release.bumpInFlight = null;
|
|
@@ -6218,7 +7109,7 @@ async function setStudioCustomReleaseVersion(localId) {
|
|
|
6218
7109
|
}, 1500);
|
|
6219
7110
|
await loadLocal();
|
|
6220
7111
|
if (state.studio.id === localId) {
|
|
6221
|
-
await
|
|
7112
|
+
await refreshStudioAfterVersionChange(localId);
|
|
6222
7113
|
}
|
|
6223
7114
|
} catch (error) {
|
|
6224
7115
|
state.studio.release.bumpInFlight = null;
|
|
@@ -6231,6 +7122,9 @@ function applyStudioVersionChangeResult(localId, result) {
|
|
|
6231
7122
|
if (result && typeof result === "object") {
|
|
6232
7123
|
const { checkState, ...versionState } = result;
|
|
6233
7124
|
state.versionStates.set(localId, versionState);
|
|
7125
|
+
if (state.studio.id === localId && versionState.localVersion) {
|
|
7126
|
+
state.studio.release.newTagDraft = versionState.localVersion;
|
|
7127
|
+
}
|
|
6234
7128
|
}
|
|
6235
7129
|
if (state.studio.id === localId && result && Object.prototype.hasOwnProperty.call(result, "checkState")) {
|
|
6236
7130
|
applyStudioCheckState(result.checkState);
|
|
@@ -6246,6 +7140,9 @@ async function refreshStudioVersionState() {
|
|
|
6246
7140
|
`/api/plugins/local/${encodeURIComponent(state.studio.id)}/version-state`
|
|
6247
7141
|
);
|
|
6248
7142
|
state.versionStates.set(state.studio.id, versionState);
|
|
7143
|
+
if (!state.studio.release.newTagDraft && versionState.localVersion) {
|
|
7144
|
+
state.studio.release.newTagDraft = versionState.localVersion;
|
|
7145
|
+
}
|
|
6249
7146
|
updateStudioSidebar();
|
|
6250
7147
|
} catch (error) {
|
|
6251
7148
|
// ignore — sidebar will keep stale data and notice was already shown
|