@widgetic/creator 0.3.60 → 0.3.62
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/dist/CreatorApp.svelte +597 -112
- package/dist/components/HoverTooltip.svelte +8 -2
- package/dist/components/HoverTooltip.svelte.d.ts +5 -0
- package/dist/components/VersionDropdownRow.svelte +101 -0
- package/dist/components/VersionDropdownRow.svelte.d.ts +29 -0
- package/dist/components/WidgetShapeLivePreviews.svelte +397 -124
- package/dist/components/WidgetShapeLivePreviews.svelte.d.ts +43 -11
- package/dist/utils/versionPreviewError.d.ts +9 -0
- package/dist/utils/versionPreviewError.js +19 -0
- package/package.json +1 -1
package/dist/CreatorApp.svelte
CHANGED
|
@@ -28,6 +28,7 @@
|
|
|
28
28
|
ResponseError
|
|
29
29
|
} from '@widgetic/api-sdk';
|
|
30
30
|
import type { Middleware } from '@widgetic/api-sdk';
|
|
31
|
+
import { parseVersionPreviewError } from './utils/versionPreviewError.js';
|
|
31
32
|
|
|
32
33
|
/**
|
|
33
34
|
* Idempotency middleware (Phase 2 hardening): the gateway rejects
|
|
@@ -465,6 +466,7 @@
|
|
|
465
466
|
import type { DropdownItem } from './components/SelectorDropdown.svelte';
|
|
466
467
|
import WidgetShapeLivePreviews from './components/WidgetShapeLivePreviews.svelte';
|
|
467
468
|
import HoverTooltip from './components/HoverTooltip.svelte';
|
|
469
|
+
import VersionDropdownRow from './components/VersionDropdownRow.svelte';
|
|
468
470
|
|
|
469
471
|
// WIDGET DETAILS COMPONENT — lazy-loaded to reduce peak build memory
|
|
470
472
|
let WidgetDetails: any;
|
|
@@ -708,6 +710,21 @@
|
|
|
708
710
|
}
|
|
709
711
|
showToast('success', 'Widget deleted from database');
|
|
710
712
|
} catch (err) {
|
|
713
|
+
// A shape can outlive its widget row (deleted from the Projects page or
|
|
714
|
+
// another session). The canvas only needs the shape and local entry gone,
|
|
715
|
+
// so treat "already gone" as success instead of a blocking error — that
|
|
716
|
+
// left orphan shapes impossible to remove from the canvas.
|
|
717
|
+
if (err instanceof ResponseError && err.response?.status === 404) {
|
|
718
|
+
console.warn('[WidgetCreator] Widget already absent from DB — removing locally:', widgetId);
|
|
719
|
+
widgets = widgets.filter((w: any) => w.id !== widgetId);
|
|
720
|
+
if (selectedWidgetId === widgetId) {
|
|
721
|
+
selectedWidgetId = null;
|
|
722
|
+
}
|
|
723
|
+
showToast('info', 'Widget was already deleted', {
|
|
724
|
+
description: 'Removed the leftover shape from this canvas.',
|
|
725
|
+
});
|
|
726
|
+
return;
|
|
727
|
+
}
|
|
711
728
|
console.error('[WidgetCreator] Failed to delete widget from DB:', err);
|
|
712
729
|
showToast('error', 'Failed to delete widget from database');
|
|
713
730
|
}
|
|
@@ -1319,14 +1336,15 @@
|
|
|
1319
1336
|
return dataUrls.slice(0, 1);
|
|
1320
1337
|
}
|
|
1321
1338
|
|
|
1322
|
-
async function persistWidgetListThumbnail(widgetId: string, imageDataUrl: string): Promise<void> {
|
|
1323
|
-
if (!widgetsClient || !widgetId || !imageDataUrl) return;
|
|
1339
|
+
async function persistWidgetListThumbnail(widgetId: string, imageDataUrl: string, isCurrent = () => true): Promise<void> {
|
|
1340
|
+
if (!widgetsClient || !widgetId || !imageDataUrl || !isCurrent()) return;
|
|
1324
1341
|
try {
|
|
1325
1342
|
const compact = imageDataUrl.startsWith('data:image/')
|
|
1326
1343
|
? await compressImageDataUrlForCodegen(imageDataUrl, 384, 180000)
|
|
1327
1344
|
: imageDataUrl;
|
|
1345
|
+
if (!isCurrent()) return;
|
|
1328
1346
|
const thumbnailUrl = await persistWidgetThumbnail(widgetsClient, widgetId, compact);
|
|
1329
|
-
if (thumbnailUrl) {
|
|
1347
|
+
if (thumbnailUrl && isCurrent()) {
|
|
1330
1348
|
widgets = widgets.map((w) =>
|
|
1331
1349
|
w.id === widgetId ? { ...w, thumbnailUrl, thumbnail_url: thumbnailUrl } : w
|
|
1332
1350
|
);
|
|
@@ -1518,19 +1536,29 @@
|
|
|
1518
1536
|
}
|
|
1519
1537
|
|
|
1520
1538
|
/** Live WidgetShape iframe only — never the hidden panel preview column. */
|
|
1539
|
+
/**
|
|
1540
|
+
* Find the live preview iframe for a widget.
|
|
1541
|
+
*
|
|
1542
|
+
* When a widget id is given we must return *that* widget's iframe or nothing
|
|
1543
|
+
* at all. Falling back to "the first widget-shape iframe on the page" meant a
|
|
1544
|
+
* screenshot request for widget A could capture widget B's preview and store it
|
|
1545
|
+
* as A's thumbnail — which is how a gallery thumbnail ended up on an unrelated
|
|
1546
|
+
* widget in the Projects list.
|
|
1547
|
+
*/
|
|
1521
1548
|
function resolveLiveWidgetShapeIframe(widgetId: string | null | undefined): HTMLIFrameElement | null {
|
|
1522
1549
|
const targetId = widgetId || focusedPanelWidgetId || selectedWidgetId;
|
|
1523
1550
|
if (targetId) {
|
|
1524
1551
|
const liveById = document.querySelector(
|
|
1525
1552
|
`iframe.widget-shape-live-preview-iframe[data-widget-id="${CSS.escape(targetId)}"]`,
|
|
1526
1553
|
) as HTMLIFrameElement | null;
|
|
1527
|
-
|
|
1554
|
+
return liveById?.contentWindow ? liveById : null;
|
|
1528
1555
|
}
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
);
|
|
1556
|
+
// No id known: only unambiguous when exactly one shape is mounted.
|
|
1557
|
+
const all = Array.from(
|
|
1558
|
+
document.querySelectorAll('iframe.widget-shape-live-preview-iframe'),
|
|
1559
|
+
) as HTMLIFrameElement[];
|
|
1560
|
+
const withWindow = all.filter((frame) => !!frame.contentWindow);
|
|
1561
|
+
return withWindow.length === 1 ? withWindow[0] : null;
|
|
1534
1562
|
}
|
|
1535
1563
|
|
|
1536
1564
|
function isWorkerPreviewUrl(url: string | null | undefined): boolean {
|
|
@@ -1559,8 +1587,8 @@
|
|
|
1559
1587
|
|
|
1560
1588
|
function handler(event: MessageEvent) {
|
|
1561
1589
|
if (event.data?.type !== 'widgetic:screenshot') return;
|
|
1562
|
-
//
|
|
1563
|
-
if (event.source
|
|
1590
|
+
// Ignore other widgets and synthetic replies with no source window.
|
|
1591
|
+
if (event.source !== iframe.contentWindow) return;
|
|
1564
1592
|
clearTimeout(timeout);
|
|
1565
1593
|
window.removeEventListener('message', handler);
|
|
1566
1594
|
resolve({
|
|
@@ -1624,15 +1652,43 @@
|
|
|
1624
1652
|
}
|
|
1625
1653
|
}
|
|
1626
1654
|
|
|
1655
|
+
/** Explicit Refresh only: capture the loaded frame without opening a panel or publishing. */
|
|
1656
|
+
async function handleWidgetShapeRefresh(widgetId, iframe, isCurrentFrame) {
|
|
1657
|
+
const capturedCanvasId = getEffectiveCanvasId();
|
|
1658
|
+
const capturedCanvas = canvasRef;
|
|
1659
|
+
const capturedSource = shapePreviewSourceByWidgetId[widgetId] || 'live';
|
|
1660
|
+
// Frame/intent identity owns this capture, not focused-panel HEAD hydration
|
|
1661
|
+
// or another widget's global generation operation.
|
|
1662
|
+
const isCurrent = () => isCurrentFrame() && !!getWidgetById(widgetId)
|
|
1663
|
+
&& !isDraftConvertId(widgetId) && !generatingWidgetIds[widgetId]
|
|
1664
|
+
&& capturedCanvasId === getEffectiveCanvasId() && capturedCanvas === canvasRef
|
|
1665
|
+
&& capturedSource === (shapePreviewSourceByWidgetId[widgetId] || 'live')
|
|
1666
|
+
&& resolveLiveWidgetShapeIframe(widgetId) === iframe;
|
|
1667
|
+
if (!isCurrent()) return;
|
|
1668
|
+
try {
|
|
1669
|
+
const screenshot = await handleChatScreenshot(widgetId, iframe);
|
|
1670
|
+
if (!screenshot || !isCurrent()) return;
|
|
1671
|
+
// Persist before Fabric/autosave side effects can resync the live overlay.
|
|
1672
|
+
await persistWidgetListThumbnail(widgetId, screenshot, isCurrent);
|
|
1673
|
+
if (!isCurrent()) return;
|
|
1674
|
+
capturedCanvas?.updateWidgetImage(widgetId, screenshot);
|
|
1675
|
+
const canvasJson = capturedCanvas?.saveCanvas?.();
|
|
1676
|
+
if (canvasJson) void handleCanvasAutoSave(canvasJson);
|
|
1677
|
+
} catch (error) {
|
|
1678
|
+
console.warn('[WidgetShape] Refresh thumbnail capture failed', widgetId, error);
|
|
1679
|
+
}
|
|
1680
|
+
}
|
|
1681
|
+
|
|
1627
1682
|
/** Screenshot: capture the live WidgetShape iframe (CDN or Worker), not Fabric wireframe. */
|
|
1628
|
-
async function handleChatScreenshot(widgetId?: string | null): Promise<string | null> {
|
|
1683
|
+
async function handleChatScreenshot(widgetId?: string | null, requiredIframe: HTMLIFrameElement | null = null): Promise<string | null> {
|
|
1629
1684
|
const targetId = widgetId || focusedPanelWidgetId || selectedWidgetId;
|
|
1630
1685
|
if (!targetId) {
|
|
1631
1686
|
console.warn('[Chat→Screenshot] No widget id');
|
|
1632
1687
|
return null;
|
|
1633
1688
|
}
|
|
1634
1689
|
|
|
1635
|
-
const liveIframe = resolveLiveWidgetShapeIframe(targetId);
|
|
1690
|
+
const liveIframe = requiredIframe ?? resolveLiveWidgetShapeIframe(targetId);
|
|
1691
|
+
if (requiredIframe && (requiredIframe !== resolveLiveWidgetShapeIframe(targetId) || !requiredIframe.isConnected)) return null;
|
|
1636
1692
|
const liveSrc = liveIframe
|
|
1637
1693
|
? liveIframe.getAttribute('src') || liveIframe.src || ''
|
|
1638
1694
|
: '';
|
|
@@ -1651,6 +1707,9 @@
|
|
|
1651
1707
|
console.warn('[Chat→Screenshot] Live iframe capture failed:', fromLive.error);
|
|
1652
1708
|
}
|
|
1653
1709
|
|
|
1710
|
+
// Explicit refresh must not persist a different version or a stale Fabric image.
|
|
1711
|
+
if (requiredIframe) return null;
|
|
1712
|
+
|
|
1654
1713
|
// 2) Hidden Worker /preview (same capture script; wait for load)
|
|
1655
1714
|
const fromWorker = await captureViaHiddenWorkerPreview(targetId);
|
|
1656
1715
|
if (fromWorker && fromWorker.length > 500) return fromWorker;
|
|
@@ -2070,11 +2129,28 @@
|
|
|
2070
2129
|
null;
|
|
2071
2130
|
// console.log('[Chat→Backend] Git rollback result:', gitRollback, { undoMode, targetCommit, panelWidgetId });
|
|
2072
2131
|
if (gitRollback?.success) {
|
|
2132
|
+
// Fast path: a recorded version at this commit has an immutable cached
|
|
2133
|
+
// artifact — reuse it instead of the forced repo rebuild + retry ladder.
|
|
2134
|
+
let previewFromVersionCache = false;
|
|
2073
2135
|
if (targetCommit) {
|
|
2074
2136
|
lastCommitId = targetCommit;
|
|
2075
2137
|
if (panelWidgetId) {
|
|
2076
2138
|
patchPanelSnapshot(panelWidgetId, { lastCommitId: targetCommit });
|
|
2077
2139
|
patchCodegenUiState(panelWidgetId, {});
|
|
2140
|
+
// Message-extracted SHAs may be short (7+ chars); compare by prefix.
|
|
2141
|
+
const targetSha = String(targetCommit).toLowerCase();
|
|
2142
|
+
const matchedVersion = (resolveWidgetShapeVersionOptions(panelWidgetId) ?? [])
|
|
2143
|
+
.find((option) => {
|
|
2144
|
+
const optionSha = String(option.commitSha ?? '').toLowerCase();
|
|
2145
|
+
return optionSha.length >= 7 && targetSha.length >= 7
|
|
2146
|
+
&& (optionSha === targetSha || optionSha.startsWith(targetSha) || targetSha.startsWith(optionSha));
|
|
2147
|
+
});
|
|
2148
|
+
if (matchedVersion) {
|
|
2149
|
+
shapeVersionPreviewErrorsByKey = { ...shapeVersionPreviewErrorsByKey, [`${panelWidgetId}:v:${matchedVersion.id.slice(2)}`]: null };
|
|
2150
|
+
shapePreviewSourceByWidgetId = { ...shapePreviewSourceByWidgetId, [panelWidgetId]: matchedVersion.id };
|
|
2151
|
+
handleWidgetShapeVersionChange(panelWidgetId, matchedVersion.id);
|
|
2152
|
+
previewFromVersionCache = true;
|
|
2153
|
+
}
|
|
2078
2154
|
}
|
|
2079
2155
|
}
|
|
2080
2156
|
showToast('success', undoMode ? 'Change undone' : 'Checkpoint restored', {
|
|
@@ -2089,7 +2165,13 @@
|
|
|
2089
2165
|
compositionDesignSchema = null;
|
|
2090
2166
|
compositionContentSchema = null;
|
|
2091
2167
|
pendingCreatePreviewDefaultsWidgetId = null;
|
|
2092
|
-
_isRestoringCheckpoint =
|
|
2168
|
+
_isRestoringCheckpoint = !previewFromVersionCache;
|
|
2169
|
+
if (previewFromVersionCache) {
|
|
2170
|
+
// Immutable artifact already serves the preview; no rebuild ladder.
|
|
2171
|
+
syncWidgetShapeLivePreviews();
|
|
2172
|
+
setTimeout(() => handleRefreshScreenshot(panelWidgetId, { quiet: true }), 2000);
|
|
2173
|
+
return { ...data, previewFromVersionCache };
|
|
2174
|
+
}
|
|
2093
2175
|
const panelRef = panelWidgetId ? getWidgetDetailsRef(panelWidgetId) : widgetDetails;
|
|
2094
2176
|
const scheduleBuildRetry = async () => {
|
|
2095
2177
|
const delays = [2500, 5000, 8000];
|
|
@@ -2178,7 +2260,7 @@
|
|
|
2178
2260
|
let lastPublishedVersion: number | null = null;
|
|
2179
2261
|
let selectedWidgetJsPath: string | null = null;
|
|
2180
2262
|
|
|
2181
|
-
//
|
|
2263
|
+
// Version history state — loaded when user expands the history panel
|
|
2182
2264
|
interface PublishHistoryRecord {
|
|
2183
2265
|
id: string;
|
|
2184
2266
|
version: number;
|
|
@@ -2187,10 +2269,23 @@
|
|
|
2187
2269
|
commit_sha?: string | null;
|
|
2188
2270
|
artifactUrl?: string;
|
|
2189
2271
|
artifact_url?: string;
|
|
2190
|
-
|
|
2191
|
-
|
|
2272
|
+
/**
|
|
2273
|
+
* When this version went Live. Null/absent means it is a Draft — a version
|
|
2274
|
+
* that has been generated but never published. This is the single
|
|
2275
|
+
* discriminator between Draft and Live; there is no second version type.
|
|
2276
|
+
*/
|
|
2277
|
+
publishedAt?: string | null;
|
|
2278
|
+
published_at?: string | null;
|
|
2192
2279
|
publishedBy?: string | null;
|
|
2193
2280
|
published_by?: string | null;
|
|
2281
|
+
createdAt?: string | null;
|
|
2282
|
+
created_at?: string | null;
|
|
2283
|
+
/** Derived server-side so the client does not re-infer the Draft/Live split. */
|
|
2284
|
+
isPublished?: boolean;
|
|
2285
|
+
is_published?: boolean;
|
|
2286
|
+
thumbnailUrl?: string | null;
|
|
2287
|
+
thumbnail_url?: string | null;
|
|
2288
|
+
prompt?: string | null;
|
|
2194
2289
|
}
|
|
2195
2290
|
let publishHistory: PublishHistoryRecord[] = [];
|
|
2196
2291
|
let isHistoryOpen = false;
|
|
@@ -5079,6 +5174,8 @@
|
|
|
5079
5174
|
sessionCanvasId = entry.id;
|
|
5080
5175
|
activeCanvasName = entry.name;
|
|
5081
5176
|
saveSelectedCanvasId(canvasId);
|
|
5177
|
+
// The deferred cooldown save (if any) holds the previous canvas's shapes.
|
|
5178
|
+
discardPendingCooldownAutosave();
|
|
5082
5179
|
// Address bar must track the canvas we actually switched to, and must not keep a
|
|
5083
5180
|
// widget_id belonging to the previous canvas.
|
|
5084
5181
|
routeCanvasId = entry.id;
|
|
@@ -5220,6 +5317,7 @@
|
|
|
5220
5317
|
function beginBlankCanvasSession(reason: string): void {
|
|
5221
5318
|
ignoreIncomingCanvasLoads = true;
|
|
5222
5319
|
canvasLoadEpoch += 1;
|
|
5320
|
+
discardPendingCooldownAutosave();
|
|
5223
5321
|
lastCommitId = null;
|
|
5224
5322
|
lastPublishedVersion = null;
|
|
5225
5323
|
selectedWidgetJsPath = null;
|
|
@@ -5230,6 +5328,19 @@
|
|
|
5230
5328
|
// console.log('[CreatorApp] Blank canvas session:', reason, 'epoch', canvasLoadEpoch);
|
|
5231
5329
|
}
|
|
5232
5330
|
|
|
5331
|
+
/**
|
|
5332
|
+
* Drop a deferred cooldown autosave. Must run whenever the active canvas
|
|
5333
|
+
* changes: the captured JSON belongs to the previous canvas id, and flushing
|
|
5334
|
+
* it later would write one canvas's shapes into another.
|
|
5335
|
+
*/
|
|
5336
|
+
function discardPendingCooldownAutosave(): void {
|
|
5337
|
+
if (cooldownAutosaveTimer) {
|
|
5338
|
+
clearTimeout(cooldownAutosaveTimer);
|
|
5339
|
+
cooldownAutosaveTimer = null;
|
|
5340
|
+
}
|
|
5341
|
+
pendingCooldownAutosaveJson = null;
|
|
5342
|
+
}
|
|
5343
|
+
|
|
5233
5344
|
function relinkSelectedWidgetShape(): void {
|
|
5234
5345
|
placeSelectedWidgetShapeOnCanvas();
|
|
5235
5346
|
}
|
|
@@ -5319,6 +5430,7 @@
|
|
|
5319
5430
|
|
|
5320
5431
|
if (createdId) {
|
|
5321
5432
|
canvasLoadEpoch += 1;
|
|
5433
|
+
discardPendingCooldownAutosave();
|
|
5322
5434
|
isCanvasStateLoaded = false;
|
|
5323
5435
|
userCanvases = [{ id: createdId, name: newName, hasContent: false }, ...userCanvases];
|
|
5324
5436
|
showCanvasDropdown = false;
|
|
@@ -6640,7 +6752,11 @@
|
|
|
6640
6752
|
...(lastCommitId ? { hasGeneratedCode: true } : {}),
|
|
6641
6753
|
});
|
|
6642
6754
|
if (lastCommitId) hasGeneratedCode = true;
|
|
6643
|
-
|
|
6755
|
+
// Fetch history whenever the widget has code (generated or published).
|
|
6756
|
+
// Gating on `lastPublishedVersion` alone meant a widget that had only
|
|
6757
|
+
// ever been generated never loaded its history — so its version picker
|
|
6758
|
+
// stayed empty even though generations were recorded server-side.
|
|
6759
|
+
if (lastPublishedVersion !== null || lastCommitId || currentRepositoryId) {
|
|
6644
6760
|
// Load history against the specific widget id (not selectedWidgetId) so the
|
|
6645
6761
|
// panel snapshot is guaranteed to be marked publishHistoryResolved even if the
|
|
6646
6762
|
// user switches widget between hydrate and API response.
|
|
@@ -7526,13 +7642,44 @@
|
|
|
7526
7642
|
// Convert drafts capture from the canvas shape, never by opening a panel.
|
|
7527
7643
|
if (!openWidgetPanelIds.includes(widgetId)) {
|
|
7528
7644
|
if (quiet || isConvertOrDraftPanelOpen()) {
|
|
7529
|
-
|
|
7530
|
-
|
|
7531
|
-
|
|
7532
|
-
|
|
7533
|
-
|
|
7534
|
-
|
|
7535
|
-
|
|
7645
|
+
// A draft convert has no widget row and no live iframe yet — the
|
|
7646
|
+
// Fabric sketch is genuinely all there is to show.
|
|
7647
|
+
if (isDraftConvertId(widgetId)) {
|
|
7648
|
+
const draftShapeDataUrl = canvasRef?.getWidgetShapeDataUrl?.(widgetId);
|
|
7649
|
+
if (draftShapeDataUrl && canvasRef) {
|
|
7650
|
+
canvasRef.updateWidgetImage(widgetId, draftShapeDataUrl);
|
|
7651
|
+
}
|
|
7652
|
+
return;
|
|
7653
|
+
}
|
|
7654
|
+
|
|
7655
|
+
// Real widget with the details panel closed: the live shape iframe
|
|
7656
|
+
// is still on the canvas. Capture from it so both the canvas image
|
|
7657
|
+
// and the project-list thumbnail track the latest preview.
|
|
7658
|
+
//
|
|
7659
|
+
// Without this the Fabric `_imageElement` set at convert time wins
|
|
7660
|
+
// forever — it is the sketch screenshot frozen at creation, so the
|
|
7661
|
+
// Projects list kept showing the initial design after later
|
|
7662
|
+
// generations and preview refreshes.
|
|
7663
|
+
if (!shapeThumbnailCaptureInFlight.has(widgetId)) {
|
|
7664
|
+
shapeThumbnailCaptureInFlight.add(widgetId);
|
|
7665
|
+
try {
|
|
7666
|
+
const freshCapture = await handleChatScreenshot(widgetId);
|
|
7667
|
+
if (freshCapture) {
|
|
7668
|
+
canvasRef?.updateWidgetImage(widgetId, freshCapture);
|
|
7669
|
+
const freshJson = canvasRef?.saveCanvas?.();
|
|
7670
|
+
if (freshJson) void handleCanvasAutoSave(freshJson);
|
|
7671
|
+
void persistWidgetListThumbnail(widgetId, freshCapture);
|
|
7672
|
+
} else {
|
|
7673
|
+
// No live iframe/Worker preview reachable — leave the
|
|
7674
|
+
// existing image alone rather than persisting a wireframe.
|
|
7675
|
+
console.warn(
|
|
7676
|
+
'[handleRefreshScreenshot] Quiet capture produced no image — keeping existing shape preview',
|
|
7677
|
+
widgetId.substring(0, 8),
|
|
7678
|
+
);
|
|
7679
|
+
}
|
|
7680
|
+
} finally {
|
|
7681
|
+
shapeThumbnailCaptureInFlight.delete(widgetId);
|
|
7682
|
+
}
|
|
7536
7683
|
}
|
|
7537
7684
|
return;
|
|
7538
7685
|
}
|
|
@@ -8186,6 +8333,18 @@ Requirements:
|
|
|
8186
8333
|
// );
|
|
8187
8334
|
}
|
|
8188
8335
|
|
|
8336
|
+
// Refresh version history after every generation so the new commit shows
|
|
8337
|
+
// up in the version menu immediately. Publishing is deliberately not
|
|
8338
|
+
// automatic — the user previews and presses Publish themselves.
|
|
8339
|
+
if (generationWidgetId) {
|
|
8340
|
+
const { [generationWidgetId]: _resolved, ...restResolved } = shapeHistoryResolvedByWidgetId;
|
|
8341
|
+
shapeHistoryResolvedByWidgetId = restResolved;
|
|
8342
|
+
void loadPublishHistoryForPanel(generationWidgetId, { silent: true }).then(() => {
|
|
8343
|
+
syncWidgetShapeLivePreviews();
|
|
8344
|
+
if (isFocusedPanel) panelDisplayRevision += 1;
|
|
8345
|
+
});
|
|
8346
|
+
}
|
|
8347
|
+
|
|
8189
8348
|
// Always reconcile GitLab HEAD after codegen (metadata.commitId can be missing/stale).
|
|
8190
8349
|
if (generationWidgetId) {
|
|
8191
8350
|
void (async () => {
|
|
@@ -8581,11 +8740,26 @@ Requirements:
|
|
|
8581
8740
|
*/
|
|
8582
8741
|
async function executePublishWidget(
|
|
8583
8742
|
publishForWidgetId?: string | null,
|
|
8584
|
-
publishOpts: { silent?: boolean; fromCodegen?: boolean } = {},
|
|
8743
|
+
publishOpts: { silent?: boolean; fromCodegen?: boolean; targetVersion?: number | null } = {},
|
|
8585
8744
|
) {
|
|
8586
8745
|
const capturedWidgetId = publishForWidgetId ?? focusedPanelWidgetId ?? selectedWidgetId;
|
|
8587
8746
|
const panelState = capturedWidgetId ? getPanelDisplayState(capturedWidgetId) : null;
|
|
8588
8747
|
const capturedCanvasId = getEffectiveCanvasId();
|
|
8748
|
+
// An explicit version publishes *that* version — it is a Draft the user
|
|
8749
|
+
// picked in the version list. Without one, the newest Draft / current
|
|
8750
|
+
// commit is promoted, which is the "publish latest" flow.
|
|
8751
|
+
// `Number(null)` is 0 (finite), so reject null/undefined/'' explicitly
|
|
8752
|
+
// rather than letting them silently become version 0.
|
|
8753
|
+
const rawTargetVersion = publishOpts.targetVersion;
|
|
8754
|
+
const targetVersion =
|
|
8755
|
+
rawTargetVersion == null || rawTargetVersion === ''
|
|
8756
|
+
? null
|
|
8757
|
+
: Number.isFinite(Number(rawTargetVersion))
|
|
8758
|
+
? Number(rawTargetVersion)
|
|
8759
|
+
: null;
|
|
8760
|
+
// Recheck after async focus (and for direct callers), never substitute HEAD
|
|
8761
|
+
// when a requested Draft is missing or no longer publishable.
|
|
8762
|
+
if (rawTargetVersion != null && getPublishGateForPanel(capturedWidgetId, rawTargetVersion).disabled) return;
|
|
8589
8763
|
let capturedRepositoryId = resolveRepositoryIdForWidget(capturedWidgetId);
|
|
8590
8764
|
if (!capturedRepositoryId && capturedWidgetId) {
|
|
8591
8765
|
const widget = getWidgetById(capturedWidgetId) ?? selectedWidget;
|
|
@@ -8594,10 +8768,16 @@ Requirements:
|
|
|
8594
8768
|
if (capturedRepositoryId) stampWidgetRepositoryId(capturedWidgetId, capturedRepositoryId);
|
|
8595
8769
|
}
|
|
8596
8770
|
}
|
|
8597
|
-
|
|
8598
|
-
|
|
8599
|
-
|
|
8600
|
-
|
|
8771
|
+
// A targeted publish builds from that version's own commit, so the server
|
|
8772
|
+
// must receive the version — sending HEAD's SHA would publish the wrong code.
|
|
8773
|
+
const targetRecord = targetVersion !== null
|
|
8774
|
+
? getShapePublishHistory(capturedWidgetId).find((row) => Number(row.version) === targetVersion)
|
|
8775
|
+
: undefined;
|
|
8776
|
+
const commitShaForPublish = targetVersion !== null
|
|
8777
|
+
? (targetRecord?.commitSha ?? targetRecord?.commit_sha ?? null)
|
|
8778
|
+
: (panelState?.lastCommitId
|
|
8779
|
+
|| (capturedWidgetId ? loadCommitIdForWidget(capturedWidgetId) : null)
|
|
8780
|
+
|| lastCommitId);
|
|
8601
8781
|
|
|
8602
8782
|
if (!capturedWidgetId || !capturedCanvasId || !capturedRepositoryId) {
|
|
8603
8783
|
showToast('error', 'Cannot publish', { description: 'Widget, canvas, and repository are required.' });
|
|
@@ -8666,7 +8846,10 @@ Requirements:
|
|
|
8666
8846
|
widgetId: capturedWidgetId,
|
|
8667
8847
|
agentWidgetBuilderPublishWidgetIdPostRequest: {
|
|
8668
8848
|
componentId,
|
|
8669
|
-
commitSha: commitShaForPublish || undefined
|
|
8849
|
+
commitSha: commitShaForPublish || undefined,
|
|
8850
|
+
// Publishing a Draft the user chose: the server rebuilds from that
|
|
8851
|
+
// version's commit and stamps it Live instead of minting a new one.
|
|
8852
|
+
version: targetVersion ?? undefined
|
|
8670
8853
|
}
|
|
8671
8854
|
});
|
|
8672
8855
|
// console.log('[Publish] Widget Builder publish completed:', publishData);
|
|
@@ -8775,6 +8958,7 @@ Requirements:
|
|
|
8775
8958
|
selectedWidgetId = null;
|
|
8776
8959
|
ignoreIncomingCanvasLoads = true;
|
|
8777
8960
|
canvasLoadEpoch += 1;
|
|
8961
|
+
discardPendingCooldownAutosave();
|
|
8778
8962
|
lastCommitId = null;
|
|
8779
8963
|
lastPublishedVersion = null;
|
|
8780
8964
|
widgetDetailsStep = 'create';
|
|
@@ -9691,18 +9875,15 @@ Requirements:
|
|
|
9691
9875
|
return resolvePublishedWidgetUrl(widgetId, version, jsPath);
|
|
9692
9876
|
}
|
|
9693
9877
|
|
|
9694
|
-
/**
|
|
9878
|
+
/** Public display origin for shape/hidden capture iframes; keep WidgetDetails fetch proxy separate. */
|
|
9695
9879
|
function widgetBuilderPreviewBaseUrl(): string {
|
|
9696
|
-
|
|
9880
|
+
// Do not proxy sandboxed display frames onto the editor's origin.
|
|
9881
|
+
return (
|
|
9697
9882
|
widgetBuilderUrl ||
|
|
9698
9883
|
import.meta.env.VITE_WIDGET_BUILDER_URL ||
|
|
9699
9884
|
import.meta.env.PUBLIC_WIDGET_BUILDER_URL ||
|
|
9700
9885
|
'https://wbuilder.widgetic.com'
|
|
9701
9886
|
).replace(/\/+$/, '');
|
|
9702
|
-
if (import.meta.env.DEV && publicBase.includes('wbuilder.widgetic.com')) {
|
|
9703
|
-
return '/__widget_builder__';
|
|
9704
|
-
}
|
|
9705
|
-
return publicBase;
|
|
9706
9887
|
}
|
|
9707
9888
|
|
|
9708
9889
|
/**
|
|
@@ -9711,19 +9892,58 @@ Requirements:
|
|
|
9711
9892
|
* Publish history is loaded without requiring chat to be open.
|
|
9712
9893
|
*/
|
|
9713
9894
|
let shapePreviewSourceByWidgetId: Record<string, string> = {};
|
|
9895
|
+
// Historical previews are never interchangeable with the shared HEAD preview.
|
|
9896
|
+
let shapeVersionPreviewByWidgetId: Record<string, {
|
|
9897
|
+
versionId: string; requestId: number; loading: boolean; error: string | null; url: string | null;
|
|
9898
|
+
errorDetails?: Awaited<ReturnType<typeof parseVersionPreviewError>> | null;
|
|
9899
|
+
}> = {};
|
|
9900
|
+
let shapeVersionPreviewRequestId = 0;
|
|
9901
|
+
/** Remember failed versions after switching rows; unrelated versions stay publishable. */
|
|
9902
|
+
let shapeVersionPreviewErrorsByKey = {};
|
|
9903
|
+
|
|
9904
|
+
function getVersionPreviewPublishBlockReason(widgetId, targetVersion = null) {
|
|
9905
|
+
const selectedState = resolveWidgetShapeVersionPreviewState(widgetId);
|
|
9906
|
+
const versionId = targetVersion == null
|
|
9907
|
+
? shapePreviewSourceByWidgetId[widgetId]
|
|
9908
|
+
: `v:${targetVersion}`;
|
|
9909
|
+
const state = selectedState?.versionId === versionId ? selectedState : null;
|
|
9910
|
+
if (state?.loading) return 'Wait for this version preview to finish building.';
|
|
9911
|
+
return state?.error || shapeVersionPreviewErrorsByKey[`${widgetId}:${versionId}`] || null;
|
|
9912
|
+
}
|
|
9913
|
+
function resolveWidgetShapeVersionPreviewState(widgetId: string) {
|
|
9914
|
+
const state = shapeVersionPreviewByWidgetId[widgetId];
|
|
9915
|
+
return state?.versionId === shapePreviewSourceByWidgetId[widgetId] ? state : null;
|
|
9916
|
+
}
|
|
9714
9917
|
let shapePublishHistoryByWidgetId: Record<string, any[]> = {};
|
|
9715
9918
|
let shapeHistoryLoadInFlight: Record<string, boolean> = {};
|
|
9919
|
+
/**
|
|
9920
|
+
* Widgets whose shape publish history has already been fetched — *including*
|
|
9921
|
+
* widgets that came back with zero rows.
|
|
9922
|
+
*
|
|
9923
|
+
* Without this, an empty result is indistinguishable from "not fetched yet"
|
|
9924
|
+
* (see the `cached.length > 0` check below), so a widget that is published
|
|
9925
|
+
* (`js_path`/`version` set) but has no `widget_publish_history` rows re-issued
|
|
9926
|
+
* the request on every render. That thrashed the network and kept remounting
|
|
9927
|
+
* the live preview — the canvas looked like it constantly refreshed.
|
|
9928
|
+
*/
|
|
9929
|
+
let shapeHistoryResolvedByWidgetId: Record<string, boolean> = {};
|
|
9716
9930
|
|
|
9717
9931
|
async function ensureShapePublishHistory(widgetId: string): Promise<void> {
|
|
9718
9932
|
if (!widgetId || shapeHistoryLoadInFlight[widgetId]) return;
|
|
9933
|
+
// Already fetched once — an empty history is a valid, final answer.
|
|
9934
|
+
if (shapeHistoryResolvedByWidgetId[widgetId]) return;
|
|
9719
9935
|
const cached = shapePublishHistoryByWidgetId[widgetId];
|
|
9720
|
-
if (cached && cached.length > 0)
|
|
9936
|
+
if (cached && cached.length > 0) {
|
|
9937
|
+
shapeHistoryResolvedByWidgetId = { ...shapeHistoryResolvedByWidgetId, [widgetId]: true };
|
|
9938
|
+
return;
|
|
9939
|
+
}
|
|
9721
9940
|
const fromPanel = getPanelDisplayState(widgetId).publishHistory;
|
|
9722
9941
|
if (fromPanel && fromPanel.length > 0) {
|
|
9723
9942
|
shapePublishHistoryByWidgetId = {
|
|
9724
9943
|
...shapePublishHistoryByWidgetId,
|
|
9725
9944
|
[widgetId]: [...fromPanel],
|
|
9726
9945
|
};
|
|
9946
|
+
shapeHistoryResolvedByWidgetId = { ...shapeHistoryResolvedByWidgetId, [widgetId]: true };
|
|
9727
9947
|
return;
|
|
9728
9948
|
}
|
|
9729
9949
|
shapeHistoryLoadInFlight = { ...shapeHistoryLoadInFlight, [widgetId]: true };
|
|
@@ -9734,6 +9954,7 @@ Requirements:
|
|
|
9734
9954
|
...shapePublishHistoryByWidgetId,
|
|
9735
9955
|
[widgetId]: [...history],
|
|
9736
9956
|
};
|
|
9957
|
+
shapeHistoryResolvedByWidgetId = { ...shapeHistoryResolvedByWidgetId, [widgetId]: true };
|
|
9737
9958
|
syncWidgetShapeLivePreviews();
|
|
9738
9959
|
} finally {
|
|
9739
9960
|
const { [widgetId]: _, ...rest } = shapeHistoryLoadInFlight;
|
|
@@ -9743,7 +9964,8 @@ Requirements:
|
|
|
9743
9964
|
|
|
9744
9965
|
function getShapePublishHistory(widgetId: string): any[] {
|
|
9745
9966
|
const cached = shapePublishHistoryByWidgetId[widgetId];
|
|
9746
|
-
|
|
9967
|
+
// Honour a cached empty array: it means the fetch already happened.
|
|
9968
|
+
if (cached) return cached;
|
|
9747
9969
|
const fromPanel = getPanelDisplayState(widgetId).publishHistory;
|
|
9748
9970
|
return fromPanel?.length ? fromPanel : [];
|
|
9749
9971
|
}
|
|
@@ -9763,13 +9985,21 @@ Requirements:
|
|
|
9763
9985
|
const record = getShapePublishHistory(widgetId).find(
|
|
9764
9986
|
(row) => Number(row.version) === version,
|
|
9765
9987
|
);
|
|
9988
|
+
// Published versions use their stable artifact; drafts require an
|
|
9989
|
+
// explicit commit-specific build. Never substitute shared HEAD.
|
|
9990
|
+
const isPublished = !!(record?.publishedAt ?? record?.published_at);
|
|
9766
9991
|
const artifact =
|
|
9767
9992
|
record?.artifactUrl ||
|
|
9768
9993
|
record?.artifact_url ||
|
|
9769
9994
|
(version === ps.lastPublishedVersion ? ps.selectedWidgetJsPath : null);
|
|
9770
|
-
|
|
9771
|
-
if (
|
|
9995
|
+
|
|
9996
|
+
if (isPublished && artifact) {
|
|
9997
|
+
const url = resolvePublishedWidgetUrl(widgetId, version, artifact);
|
|
9998
|
+
if (url) return url;
|
|
9999
|
+
}
|
|
10000
|
+
return resolveWidgetShapeVersionPreviewState(widgetId)?.url ?? null;
|
|
9772
10001
|
}
|
|
10002
|
+
return null;
|
|
9773
10003
|
}
|
|
9774
10004
|
|
|
9775
10005
|
// After codegen (unpublished), prefer Worker preview over the Live CDN artifact.
|
|
@@ -9788,7 +10018,8 @@ Requirements:
|
|
|
9788
10018
|
const liveVersion =
|
|
9789
10019
|
ps.lastPublishedVersion ?? resolvePublishedVersionFromWidget(getWidgetById(widgetId));
|
|
9790
10020
|
if (source.startsWith('v:')) {
|
|
9791
|
-
const version = source.slice(2);
|
|
10021
|
+
const version = parseInt(source.slice(2), 10);
|
|
10022
|
+
if (!Number.isFinite(version)) return null;
|
|
9792
10023
|
return `v${version}`;
|
|
9793
10024
|
}
|
|
9794
10025
|
return liveVersion != null ? `v${liveVersion}` : null;
|
|
@@ -9818,10 +10049,15 @@ Requirements:
|
|
|
9818
10049
|
shortLabel?: string;
|
|
9819
10050
|
dateLabel?: string;
|
|
9820
10051
|
isLive?: boolean;
|
|
10052
|
+
isDraft?: boolean;
|
|
10053
|
+
/** Immutable commit; Restore Checkpoint reuses this artifact when it matches. */
|
|
10054
|
+
commitSha?: string;
|
|
9821
10055
|
}> | null {
|
|
9822
10056
|
const ps = getPanelDisplayState(widgetId);
|
|
9823
10057
|
const liveVersion =
|
|
9824
10058
|
ps.lastPublishedVersion ?? resolvePublishedVersionFromWidget(getWidgetById(widgetId));
|
|
10059
|
+
// Newest first. Versions are one ascending sequence, so the number orders
|
|
10060
|
+
// them correctly on its own; sorting by time is a stable tiebreak.
|
|
9825
10061
|
const history = [...getShapePublishHistory(widgetId)].sort(
|
|
9826
10062
|
(a, b) => Number(b.version) - Number(a.version),
|
|
9827
10063
|
);
|
|
@@ -9831,16 +10067,24 @@ Requirements:
|
|
|
9831
10067
|
shortLabel?: string;
|
|
9832
10068
|
dateLabel?: string;
|
|
9833
10069
|
isLive?: boolean;
|
|
10070
|
+
isDraft?: boolean;
|
|
9834
10071
|
}> = [];
|
|
9835
10072
|
for (const record of history) {
|
|
9836
10073
|
const version = Number(record.version);
|
|
9837
10074
|
if (!Number.isFinite(version)) continue;
|
|
10075
|
+
// One version sequence: a version is a Draft until it is published.
|
|
10076
|
+
// `published_at` is the discriminator, so there is no second numbering
|
|
10077
|
+
// scheme and no `gN` prefix — the history reads as plain `v1, v2, v3…`.
|
|
10078
|
+
const isPublished = !!(record.publishedAt ?? record.published_at);
|
|
9838
10079
|
options.push({
|
|
9839
10080
|
id: `v:${version}`,
|
|
9840
10081
|
label: `Version ${version}`,
|
|
9841
10082
|
shortLabel: `v${version}`,
|
|
9842
10083
|
dateLabel: formatPublishHistoryDate(record),
|
|
9843
|
-
|
|
10084
|
+
// Only a published version can be what visitors see.
|
|
10085
|
+
isLive: isPublished && version === liveVersion,
|
|
10086
|
+
isDraft: !isPublished,
|
|
10087
|
+
commitSha: record.commitSha ?? record.commit_sha,
|
|
9844
10088
|
});
|
|
9845
10089
|
}
|
|
9846
10090
|
if (options.length === 0 && liveVersion != null) {
|
|
@@ -9852,8 +10096,10 @@ Requirements:
|
|
|
9852
10096
|
isLive: true,
|
|
9853
10097
|
});
|
|
9854
10098
|
}
|
|
9855
|
-
// Kick off history fetch so the dropdown fills even before chat opens
|
|
9856
|
-
|
|
10099
|
+
// Kick off history fetch so the dropdown fills even before chat opens.
|
|
10100
|
+
// `ensureShapePublishHistory` is idempotent per widget (it records resolved
|
|
10101
|
+
// widgets, including empty results), so calling it from render is safe.
|
|
10102
|
+
if (history.length === 0) {
|
|
9857
10103
|
void ensureShapePublishHistory(widgetId);
|
|
9858
10104
|
}
|
|
9859
10105
|
return options.length > 0 ? options : null;
|
|
@@ -9868,15 +10114,102 @@ Requirements:
|
|
|
9868
10114
|
return liveVersion != null ? `v:${liveVersion}` : null;
|
|
9869
10115
|
}
|
|
9870
10116
|
|
|
9871
|
-
function handleWidgetShapeVersionChange(widgetId: string, versionId: string): void {
|
|
10117
|
+
function handleWidgetShapeVersionChange(widgetId: string, versionId: string, buildPreview = true): void {
|
|
9872
10118
|
if (!versionId) return;
|
|
10119
|
+
// Re-selecting an already successful/in-flight preview must not reset its state.
|
|
10120
|
+
const currentPreview = resolveWidgetShapeVersionPreviewState(widgetId);
|
|
10121
|
+
if (shapePreviewSourceByWidgetId[widgetId] === versionId
|
|
10122
|
+
&& (currentPreview?.loading || currentPreview?.url)) return;
|
|
9873
10123
|
shapePreviewSourceByWidgetId = {
|
|
9874
10124
|
...shapePreviewSourceByWidgetId,
|
|
9875
10125
|
[widgetId]: versionId,
|
|
9876
10126
|
};
|
|
10127
|
+
shapeVersionPreviewRequestId += 1;
|
|
10128
|
+
shapeVersionPreviewByWidgetId = {
|
|
10129
|
+
...shapeVersionPreviewByWidgetId,
|
|
10130
|
+
[widgetId]: {
|
|
10131
|
+
versionId, requestId: shapeVersionPreviewRequestId,
|
|
10132
|
+
loading: false, error: null, url: null,
|
|
10133
|
+
},
|
|
10134
|
+
};
|
|
9877
10135
|
syncWidgetShapeLivePreviews();
|
|
9878
10136
|
panelDisplayRevision += 1;
|
|
9879
|
-
|
|
10137
|
+
// Explicit Publish builds its own target; do not race it with a preview build.
|
|
10138
|
+
if (!buildPreview) return;
|
|
10139
|
+
// Published artifacts need no build. Drafts must never substitute HEAD.
|
|
10140
|
+
if (versionId.startsWith('v:')) {
|
|
10141
|
+
const version = Number(versionId.slice(2));
|
|
10142
|
+
if (resolveWidgetShapeLivePreviewUrl(widgetId)) {
|
|
10143
|
+
const record = getShapePublishHistory(widgetId).find((row) => Number(row.version) === version);
|
|
10144
|
+
console.log('[VersionPreview] Selection', { widgetId, version, commitSha: record?.commitSha ?? record?.commit_sha ?? null, source: 'published' });
|
|
10145
|
+
} else {
|
|
10146
|
+
void ensureWidgetShapeVersionPreview(widgetId, version);
|
|
10147
|
+
}
|
|
10148
|
+
}
|
|
10149
|
+
}
|
|
10150
|
+
|
|
10151
|
+
/** Explicit retry retains the selected version and its remembered publish block. */
|
|
10152
|
+
async function retryWidgetShapeVersionPreview(widgetId) {
|
|
10153
|
+
const state = resolveWidgetShapeVersionPreviewState(widgetId);
|
|
10154
|
+
if (!state?.error || state.loading || !state.versionId.startsWith('v:')) return;
|
|
10155
|
+
await ensureWidgetShapeVersionPreview(widgetId, Number(state.versionId.slice(2)), true);
|
|
10156
|
+
}
|
|
10157
|
+
|
|
10158
|
+
async function ensureWidgetShapeVersionPreview(widgetId: string, version: number, retry = false): Promise<void> {
|
|
10159
|
+
if (!widgetId || !Number.isSafeInteger(version) || version < 1) return;
|
|
10160
|
+
const versionId = `v:${version}`;
|
|
10161
|
+
if (shapePreviewSourceByWidgetId[widgetId] !== versionId) return;
|
|
10162
|
+
const previous = shapeVersionPreviewByWidgetId[widgetId];
|
|
10163
|
+
if (previous?.versionId === versionId && (previous.loading || (!retry && (previous.url || previous.error)))) return;
|
|
10164
|
+
const requestId = ++shapeVersionPreviewRequestId;
|
|
10165
|
+
shapeVersionPreviewByWidgetId = {
|
|
10166
|
+
...shapeVersionPreviewByWidgetId,
|
|
10167
|
+
[widgetId]: { versionId, requestId, loading: true, url: null,
|
|
10168
|
+
error: retry ? previous?.error ?? null : null,
|
|
10169
|
+
errorDetails: retry ? previous?.errorDetails ?? null : null },
|
|
10170
|
+
};
|
|
10171
|
+
syncWidgetShapeLivePreviews();
|
|
10172
|
+
panelDisplayRevision += 1;
|
|
10173
|
+
try {
|
|
10174
|
+
const response = await agentClient.previewWidgetVersion({ widgetId, version });
|
|
10175
|
+
const previewUrl = response?.data?.previewUrl ?? null;
|
|
10176
|
+
const commitSha = response?.data?.commitSha ?? null;
|
|
10177
|
+
// Drop stale responses: the selector may have moved on (or away) meanwhile.
|
|
10178
|
+
if (
|
|
10179
|
+
shapePreviewSourceByWidgetId[widgetId] !== versionId
|
|
10180
|
+
|| shapeVersionPreviewByWidgetId[widgetId]?.requestId !== requestId
|
|
10181
|
+
) {
|
|
10182
|
+
return;
|
|
10183
|
+
}
|
|
10184
|
+
if (!previewUrl) {
|
|
10185
|
+
throw new Error('Preview URL missing from response');
|
|
10186
|
+
}
|
|
10187
|
+
shapeVersionPreviewByWidgetId = {
|
|
10188
|
+
...shapeVersionPreviewByWidgetId,
|
|
10189
|
+
[widgetId]: { versionId, requestId, loading: false, error: null, url: previewUrl },
|
|
10190
|
+
};
|
|
10191
|
+
syncWidgetShapeLivePreviews();
|
|
10192
|
+
panelDisplayRevision += 1;
|
|
10193
|
+
shapeVersionPreviewErrorsByKey = { ...shapeVersionPreviewErrorsByKey, [`${widgetId}:${versionId}`]: null };
|
|
10194
|
+
console.log('[VersionPreview] Selection', { widgetId, version, commitSha });
|
|
10195
|
+
} catch (error: unknown) {
|
|
10196
|
+
if (
|
|
10197
|
+
shapePreviewSourceByWidgetId[widgetId] !== versionId
|
|
10198
|
+
|| shapeVersionPreviewByWidgetId[widgetId]?.requestId !== requestId
|
|
10199
|
+
) return;
|
|
10200
|
+
const errorDetails = await parseVersionPreviewError(error);
|
|
10201
|
+
if (shapePreviewSourceByWidgetId[widgetId] !== versionId
|
|
10202
|
+
|| shapeVersionPreviewByWidgetId[widgetId]?.requestId !== requestId) return;
|
|
10203
|
+
shapeVersionPreviewErrorsByKey = { ...shapeVersionPreviewErrorsByKey, [`${widgetId}:${versionId}`]: errorDetails.message };
|
|
10204
|
+
// One diagnostic per failed request; raw server wording never reaches the overlay.
|
|
10205
|
+
console.warn('[VersionPreview] Selection', { widgetId, version, ...errorDetails });
|
|
10206
|
+
shapeVersionPreviewByWidgetId = {
|
|
10207
|
+
...shapeVersionPreviewByWidgetId,
|
|
10208
|
+
[widgetId]: { versionId, requestId, loading: false, error: errorDetails.message, errorDetails, url: null },
|
|
10209
|
+
};
|
|
10210
|
+
syncWidgetShapeLivePreviews();
|
|
10211
|
+
panelDisplayRevision += 1;
|
|
10212
|
+
}
|
|
9880
10213
|
}
|
|
9881
10214
|
|
|
9882
10215
|
/** Commit SHA for the version currently selected in the version menu (drives Head-at-commit badge). */
|
|
@@ -10868,6 +11201,14 @@ Requirements:
|
|
|
10868
11201
|
function hasUnpublishedChangesForPanel(widgetId: string): boolean {
|
|
10869
11202
|
const ps = getPanelDisplayState(widgetId);
|
|
10870
11203
|
if (ps.pendingPublishAfterCodegen) return true;
|
|
11204
|
+
|
|
11205
|
+
// Never published but code exists: this is the first publishable version.
|
|
11206
|
+
// Without this the panel showed a "Draft" badge that could not be acted on —
|
|
11207
|
+
// there was no way to promote the first generation to Live.
|
|
11208
|
+
if (ps.lastPublishedVersion === null) {
|
|
11209
|
+
return panelHasPublishableCode(widgetId, ps);
|
|
11210
|
+
}
|
|
11211
|
+
|
|
10871
11212
|
const pendingSha = loadPendingPublishShaForWidget(widgetId);
|
|
10872
11213
|
const headSha = resolvePublishHeadCommitId(widgetId, ps);
|
|
10873
11214
|
if (!headSha || ps.lastPublishedVersion === null) return false;
|
|
@@ -10959,7 +11300,7 @@ Requirements:
|
|
|
10959
11300
|
* Single source of truth for Publish button label / disabled / toast reason.
|
|
10960
11301
|
* Prevents "Publish new vN" while click toasts "already live".
|
|
10961
11302
|
*/
|
|
10962
|
-
function getPublishGateForPanel(widgetId: string): {
|
|
11303
|
+
function getPublishGateForPanel(widgetId: string, targetVersion = null): {
|
|
10963
11304
|
disabled: boolean;
|
|
10964
11305
|
label: string;
|
|
10965
11306
|
reason: string | null;
|
|
@@ -11024,6 +11365,27 @@ Requirements:
|
|
|
11024
11365
|
showDisabledHint: true,
|
|
11025
11366
|
};
|
|
11026
11367
|
}
|
|
11368
|
+
const previewBlockReason = getVersionPreviewPublishBlockReason(widgetId, targetVersion);
|
|
11369
|
+
if (previewBlockReason) {
|
|
11370
|
+
return { disabled: true, label: 'Publish', reason: previewBlockReason, showDisabledHint: true };
|
|
11371
|
+
}
|
|
11372
|
+
// An explicit Draft owns its commit: HEAD being Live must not block it.
|
|
11373
|
+
// Keep all shared auth/canvas/repository/concurrency checks above this branch.
|
|
11374
|
+
if (targetVersion !== null) {
|
|
11375
|
+
const record = getShapePublishHistory(widgetId).find(
|
|
11376
|
+
(row) => Number(row.version) === Number(targetVersion),
|
|
11377
|
+
);
|
|
11378
|
+
const validVersion = targetVersion !== '' && Number.isInteger(Number(targetVersion)) && Number(targetVersion) > 0;
|
|
11379
|
+
const reason = !validVersion || !record
|
|
11380
|
+
? 'The selected draft version was not found.'
|
|
11381
|
+
: (record.publishedAt ?? record.published_at)
|
|
11382
|
+
? 'This version is already published.'
|
|
11383
|
+
: !(record.commitSha ?? record.commit_sha)
|
|
11384
|
+
? 'This draft has no recorded commit to publish.'
|
|
11385
|
+
: null;
|
|
11386
|
+
return { disabled: !!reason, label: 'Publish', reason, showDisabledHint: !!reason };
|
|
11387
|
+
}
|
|
11388
|
+
|
|
11027
11389
|
if (!hasCode) {
|
|
11028
11390
|
return {
|
|
11029
11391
|
disabled: true,
|
|
@@ -11111,6 +11473,31 @@ Requirements:
|
|
|
11111
11473
|
return 'No new version to publish';
|
|
11112
11474
|
}
|
|
11113
11475
|
|
|
11476
|
+
/**
|
|
11477
|
+
* Whether a specific Draft can be published.
|
|
11478
|
+
*
|
|
11479
|
+
* The main publish gate answers "is there newer code than Live?", which is
|
|
11480
|
+
* about HEAD. Selecting an older Draft in the version list is a different
|
|
11481
|
+
* question — that row has its own commit and can be promoted regardless of
|
|
11482
|
+
* where HEAD sits. Drafts with no recorded commit (a row that predates commit
|
|
11483
|
+
* tracking) cannot be rebuilt, so they are excluded rather than published
|
|
11484
|
+
* from whatever HEAD happens to contain.
|
|
11485
|
+
*/
|
|
11486
|
+
function canPublishDraftVersionForPanel(widgetId: string, version: number | null | undefined): boolean {
|
|
11487
|
+
if (version == null || version === '' || !Number.isFinite(Number(version))) return false;
|
|
11488
|
+
if (isPublishing && publishingWidgetId === widgetId) return false;
|
|
11489
|
+
if (isPublishing && publishingWidgetId !== null && publishingWidgetId !== widgetId) return false;
|
|
11490
|
+
if (generatingWidgetIds[widgetId]) return false;
|
|
11491
|
+
if (!widgetId || !getEffectiveCanvasId()) return false;
|
|
11492
|
+
if (!hasValidToken || !$userSession?.user) return false;
|
|
11493
|
+
const record = getPanelDisplayState(widgetId).publishHistory.find(
|
|
11494
|
+
(row) => Number(row.version) === Number(version),
|
|
11495
|
+
);
|
|
11496
|
+
if (!record) return false;
|
|
11497
|
+
if (record.publishedAt ?? record.published_at) return false;
|
|
11498
|
+
return !!(record.commitSha ?? record.commit_sha);
|
|
11499
|
+
}
|
|
11500
|
+
|
|
11114
11501
|
function isPublishDisabledForPanel(widgetId: string): boolean {
|
|
11115
11502
|
return getPublishGateForPanel(widgetId).disabled;
|
|
11116
11503
|
}
|
|
@@ -11145,7 +11532,12 @@ Requirements:
|
|
|
11145
11532
|
);
|
|
11146
11533
|
}
|
|
11147
11534
|
|
|
11148
|
-
|
|
11535
|
+
/**
|
|
11536
|
+
* Publish for a panel. Pass `targetVersion` to publish a specific Draft the
|
|
11537
|
+
* user selected in the version list (built from that version's own commit);
|
|
11538
|
+
* omit it to publish the newest generated code.
|
|
11539
|
+
*/
|
|
11540
|
+
function runPublishForPanel(panelWidgetId: string, targetVersion?: number | null): void {
|
|
11149
11541
|
void (async () => {
|
|
11150
11542
|
if (isPublishing && !publishingWidgetId) {
|
|
11151
11543
|
console.warn('[Publish] Clearing stuck global isPublishing flag');
|
|
@@ -11158,7 +11550,7 @@ Requirements:
|
|
|
11158
11550
|
|
|
11159
11551
|
// Gate BEFORE focus/tick — focusing can reconcile HEAD/live SHA mid-click and
|
|
11160
11552
|
// used to toast "already live" after the button still said "Publish new vN".
|
|
11161
|
-
const gateBefore = getPublishGateForPanel(panelWidgetId);
|
|
11553
|
+
const gateBefore = getPublishGateForPanel(panelWidgetId, targetVersion ?? null);
|
|
11162
11554
|
if (gateBefore.disabled) {
|
|
11163
11555
|
if (gateBefore.reason) {
|
|
11164
11556
|
showToast('info', 'Cannot publish yet', { description: gateBefore.reason, duration: 5000 });
|
|
@@ -11171,7 +11563,7 @@ Requirements:
|
|
|
11171
11563
|
await tick();
|
|
11172
11564
|
}
|
|
11173
11565
|
|
|
11174
|
-
const gateAfter = getPublishGateForPanel(panelWidgetId);
|
|
11566
|
+
const gateAfter = getPublishGateForPanel(panelWidgetId, targetVersion ?? null);
|
|
11175
11567
|
if (gateAfter.disabled) {
|
|
11176
11568
|
if (gateAfter.reason) {
|
|
11177
11569
|
showToast('info', 'Cannot publish yet', { description: gateAfter.reason, duration: 5000 });
|
|
@@ -11179,7 +11571,7 @@ Requirements:
|
|
|
11179
11571
|
return;
|
|
11180
11572
|
}
|
|
11181
11573
|
|
|
11182
|
-
await executePublishWidget(panelWidgetId);
|
|
11574
|
+
await executePublishWidget(panelWidgetId, { targetVersion: targetVersion ?? null });
|
|
11183
11575
|
})();
|
|
11184
11576
|
}
|
|
11185
11577
|
|
|
@@ -11321,6 +11713,9 @@ async function loadPublishHistoryForPanel(widgetId: string, opts: { silent?: boo
|
|
|
11321
11713
|
...shapePublishHistoryByWidgetId,
|
|
11322
11714
|
[widgetId]: [...history],
|
|
11323
11715
|
};
|
|
11716
|
+
// Mark resolved even when empty — an empty history is a final answer and
|
|
11717
|
+
// must stop `ensureShapePublishHistory` from re-fetching on every render.
|
|
11718
|
+
shapeHistoryResolvedByWidgetId = { ...shapeHistoryResolvedByWidgetId, [widgetId]: true };
|
|
11324
11719
|
if (widgetId === focusedPanelWidgetId) {
|
|
11325
11720
|
publishHistory = history;
|
|
11326
11721
|
publishHistoryResolved = true;
|
|
@@ -11360,11 +11755,26 @@ async function loadPublishHistory(opts: { silent?: boolean } = {}) {
|
|
|
11360
11755
|
}
|
|
11361
11756
|
|
|
11362
11757
|
/**
|
|
11363
|
-
* Sets
|
|
11364
|
-
* Updates widgets.js_path to point to
|
|
11758
|
+
* Sets an already-published version as the current live version without rebuilding.
|
|
11759
|
+
* Updates widgets.js_path to point to that version's artifact URL.
|
|
11760
|
+
*
|
|
11761
|
+
* Only published versions can be promoted — a Draft has never been built to a
|
|
11762
|
+
* stable artifact. Publishing a Draft goes through the normal Publish flow, which
|
|
11763
|
+
* is why the UI never offers "Set live" for one.
|
|
11365
11764
|
*/
|
|
11366
11765
|
async function setLiveVersionForWidget(widgetId: string, version: number) {
|
|
11367
11766
|
if (!widgetId || !$userSession?.jwt) return;
|
|
11767
|
+
if (!Number.isFinite(version)) return;
|
|
11768
|
+
|
|
11769
|
+
const record = getPanelDisplayState(widgetId).publishHistory.find(
|
|
11770
|
+
(row) => Number(row.version) === version,
|
|
11771
|
+
);
|
|
11772
|
+
if (record && !(record.publishedAt ?? record.published_at)) {
|
|
11773
|
+
showToast('info', 'This is a draft version', {
|
|
11774
|
+
description: 'Press Publish to make the latest generated code live.',
|
|
11775
|
+
});
|
|
11776
|
+
return;
|
|
11777
|
+
}
|
|
11368
11778
|
|
|
11369
11779
|
isSettingLiveVersion = version;
|
|
11370
11780
|
if (!widgetsClient) {
|
|
@@ -11419,12 +11829,17 @@ async function setLiveVersion(version: number) {
|
|
|
11419
11829
|
await setLiveVersionForWidget(selectedWidgetId, version);
|
|
11420
11830
|
}
|
|
11421
11831
|
|
|
11422
|
-
/** Toggle publish history panel — reloads data on every open
|
|
11832
|
+
/** Toggle publish history panel — reloads data on every open.
|
|
11833
|
+
* Right-aligns the dropdown with the whole header cluster (vN trigger +
|
|
11834
|
+
* LIVE/PUBLISH badge), not just the trigger button, per UX request. */
|
|
11423
11835
|
function togglePublishHistory(triggerEl?: HTMLElement | null) {
|
|
11424
11836
|
isHistoryOpen = !isHistoryOpen;
|
|
11425
11837
|
if (isHistoryOpen) {
|
|
11426
11838
|
if (triggerEl) {
|
|
11427
|
-
|
|
11839
|
+
// The vN trigger sits left of the LIVE/PUBLISH badge inside the same
|
|
11840
|
+
// header cluster; align the panel to that cluster's right edge.
|
|
11841
|
+
const cluster = triggerEl.closest('.wc-version-history-ct') as HTMLElement | null;
|
|
11842
|
+
const rect = (cluster ?? triggerEl).getBoundingClientRect();
|
|
11428
11843
|
liveHistoryPanelTop = Math.round(rect.bottom + 4);
|
|
11429
11844
|
liveHistoryPanelRight = Math.round(window.innerWidth - rect.right);
|
|
11430
11845
|
}
|
|
@@ -11911,6 +12326,13 @@ $: if (codeGenerationPrompt && selectedWidgetId && promptLoadedForWidgetId === s
|
|
|
11911
12326
|
let lastWidgetShapeClickId = '';
|
|
11912
12327
|
const WIDGET_SHAPE_CLICK_DEBOUNCE_MS = 450;
|
|
11913
12328
|
|
|
12329
|
+
/**
|
|
12330
|
+
* Widgets whose live-iframe thumbnail capture is currently running.
|
|
12331
|
+
* Prevents overlapping captures when several generation/refresh events fire
|
|
12332
|
+
* close together for the same widget.
|
|
12333
|
+
*/
|
|
12334
|
+
const shapeThumbnailCaptureInFlight = new Set<string>();
|
|
12335
|
+
|
|
11914
12336
|
/**
|
|
11915
12337
|
* Called when a Widget Image shape is clicked on the canvas.
|
|
11916
12338
|
* Selects the linked widget and opens WidgetDetails.
|
|
@@ -12549,6 +12971,14 @@ $: if (codeGenerationPrompt && selectedWidgetId && promptLoadedForWidgetId === s
|
|
|
12549
12971
|
*/
|
|
12550
12972
|
let autosaveSequence = 0;
|
|
12551
12973
|
let lastResolvedAutosaveSequence = 0;
|
|
12974
|
+
/**
|
|
12975
|
+
* Trailing save deferred while `isLoadCooldownActive` is set.
|
|
12976
|
+
* See {@link handleCanvasAutoSave}: dropping cooldown saves lost user edits.
|
|
12977
|
+
*/
|
|
12978
|
+
let pendingCooldownAutosaveJson: string | null = null;
|
|
12979
|
+
let cooldownAutosaveTimer: ReturnType<typeof setTimeout> | null = null;
|
|
12980
|
+
/** How long after a canvas load we still defer (not drop) autosaves. */
|
|
12981
|
+
const CANVAS_COOLDOWN_AUTOSAVE_DELAY_MS = 3000;
|
|
12552
12982
|
|
|
12553
12983
|
async function handleCanvasAutoSave(
|
|
12554
12984
|
json: string | null,
|
|
@@ -12571,8 +13001,21 @@ $: if (codeGenerationPrompt && selectedWidgetId && promptLoadedForWidgetId === s
|
|
|
12571
13001
|
}
|
|
12572
13002
|
|
|
12573
13003
|
// Guard: skip autosaves triggered by loadCanvas() object:added events
|
|
13004
|
+
//
|
|
13005
|
+
// Dropping the save outright loses any edit the user made during the cooldown
|
|
13006
|
+
// (canvas hydration, shape restore, and `loadCanvas`'s object:added events all
|
|
13007
|
+
// run inside it). Defer one trailing save instead: the load burst still cannot
|
|
13008
|
+
// overwrite freshly painted state, but a real move/resize a moment later is
|
|
13009
|
+
// persisted rather than silently discarded.
|
|
12574
13010
|
if (!force && isLoadCooldownActive && !isUnmountSave) {
|
|
12575
|
-
|
|
13011
|
+
pendingCooldownAutosaveJson = json;
|
|
13012
|
+
if (cooldownAutosaveTimer) clearTimeout(cooldownAutosaveTimer);
|
|
13013
|
+
cooldownAutosaveTimer = setTimeout(() => {
|
|
13014
|
+
cooldownAutosaveTimer = null;
|
|
13015
|
+
const deferred = pendingCooldownAutosaveJson;
|
|
13016
|
+
pendingCooldownAutosaveJson = null;
|
|
13017
|
+
if (deferred) void handleCanvasAutoSave(deferred);
|
|
13018
|
+
}, CANVAS_COOLDOWN_AUTOSAVE_DELAY_MS);
|
|
12576
13019
|
return;
|
|
12577
13020
|
}
|
|
12578
13021
|
|
|
@@ -13578,6 +14021,7 @@ $: if (codeGenerationPrompt && selectedWidgetId && promptLoadedForWidgetId === s
|
|
|
13578
14021
|
<svelte:component this={WidgeticCanvas}
|
|
13579
14022
|
bind:this={canvasRef}
|
|
13580
14023
|
showContainerFrame={!embedded}
|
|
14024
|
+
widgetSelectionLayerTarget="floating-panels-overlay"
|
|
13581
14025
|
bind:wireframeMode={canvasWireframeMode}
|
|
13582
14026
|
advancedFeatures={canvasAdvancedFeatures}
|
|
13583
14027
|
onReady={handleCanvasReady}
|
|
@@ -13628,6 +14072,12 @@ $: if (codeGenerationPrompt && selectedWidgetId && promptLoadedForWidgetId === s
|
|
|
13628
14072
|
bind:this={widgetShapeLivePreviewsRef}
|
|
13629
14073
|
getRects={() => canvasRef?.listWidgetShapeScreenRects?.() ?? []}
|
|
13630
14074
|
resolveUrl={resolveWidgetShapeLivePreviewUrl}
|
|
14075
|
+
resolvePreviewState={resolveWidgetShapeVersionPreviewState}
|
|
14076
|
+
onRetryPreview={retryWidgetShapeVersionPreview}
|
|
14077
|
+
primaryWidgetId={canvasPrimaryWidgetId}
|
|
14078
|
+
onSetPrimary={(widgetId) => handleHostSetPrimaryWidgetEvent(new CustomEvent(HOST_SET_PRIMARY_WIDGET_EVENT, { detail: { widgetId } }))}
|
|
14079
|
+
resolvePublishDisabled={(widgetId, version) => getPublishGateForPanel(widgetId, version ?? null).disabled}
|
|
14080
|
+
resolvePublishDisabledReason={(widgetId, version) => getPublishGateForPanel(widgetId, version ?? null).reason}
|
|
13631
14081
|
resolveTitle={getWidgetNameById}
|
|
13632
14082
|
titleRevision={widgetNamesRevision + panelDisplayRevision}
|
|
13633
14083
|
resolveVersionLabel={resolveWidgetShapeVersionLabel}
|
|
@@ -13639,8 +14089,10 @@ $: if (codeGenerationPrompt && selectedWidgetId && promptLoadedForWidgetId === s
|
|
|
13639
14089
|
}}
|
|
13640
14090
|
settingLiveVersion={isSettingLiveVersion}
|
|
13641
14091
|
resolveCanPublish={(widgetId) => hasUnpublishedChangesForPanel(widgetId)}
|
|
14092
|
+
resolveCanPublishDraft={(widgetId, version) => canPublishDraftVersionForPanel(widgetId, version)}
|
|
13642
14093
|
resolveIsPublishing={(widgetId) => isPublishing && publishingWidgetId === widgetId}
|
|
13643
14094
|
onPublish={(widgetId) => runPublishForPanel(widgetId)}
|
|
14095
|
+
onPublishDraft={(widgetId, version) => runPublishForPanel(widgetId, version)}
|
|
13644
14096
|
selectedWidgetId={canvasSelectedWidgetShapeId}
|
|
13645
14097
|
onTranslate={(widgetId, dx, dy) => {
|
|
13646
14098
|
canvasRef?.translateWidgetShapeByScreen?.(widgetId, dx, dy);
|
|
@@ -13656,9 +14108,7 @@ $: if (codeGenerationPrompt && selectedWidgetId && promptLoadedForWidgetId === s
|
|
|
13656
14108
|
onOpenChat={(widgetId) => {
|
|
13657
14109
|
void openWidgetDetailsPanel(widgetId);
|
|
13658
14110
|
}}
|
|
13659
|
-
onRefresh={
|
|
13660
|
-
console.log('[WidgetShape] Iframe refreshed', widgetId.substring(0, 8));
|
|
13661
|
-
}}
|
|
14111
|
+
onRefresh={handleWidgetShapeRefresh}
|
|
13662
14112
|
/>
|
|
13663
14113
|
|
|
13664
14114
|
<!-- ═══ PANEL 2: Widgets Panel — debug + showWidgetsList (site passes false) ═══ -->
|
|
@@ -13952,6 +14402,16 @@ $: if (codeGenerationPrompt && selectedWidgetId && promptLoadedForWidgetId === s
|
|
|
13952
14402
|
{@const panelWidget = widgetsById.get(panelWidgetId) ?? null}
|
|
13953
14403
|
{@const isPanelFocused = focusedPanelWidgetId === panelWidgetId}
|
|
13954
14404
|
{@const ps = (panelDisplayRevision, getPanelDisplayState(panelWidgetId))}
|
|
14405
|
+
<!--
|
|
14406
|
+
Same published-version resolution the WidgetDetails instance below gets.
|
|
14407
|
+
`ps.lastPublishedVersion` alone is only hydrated from the focused panel /
|
|
14408
|
+
DB snapshot; a widget whose summary carries `jsPath` (/v<N>/) or `version`
|
|
14409
|
+
can be published while `ps.lastPublishedVersion` is still null. The header
|
|
14410
|
+
version menu must not disagree with the panel's own Publish/Live state.
|
|
14411
|
+
-->
|
|
14412
|
+
{@const panelPublishedVersion = isDraftConvertId(panelWidgetId)
|
|
14413
|
+
? null
|
|
14414
|
+
: (ps.lastPublishedVersion ?? resolvePublishedVersionFromWidget(panelWidget ?? null))}
|
|
13955
14415
|
{@const panelHasUnpublishedChanges = (panelDisplayRevision, widgets.length, hasUnpublishedChangesForPanel(panelWidgetId))}
|
|
13956
14416
|
{@const panelHasCompileErrorState = (panelDisplayRevision, panelHasCompileError(panelWidgetId))}
|
|
13957
14417
|
{@const panelHasInfraPreviewError = (panelDisplayRevision, panelHasInfrastructurePreviewError(panelWidgetId))}
|
|
@@ -14272,14 +14732,37 @@ $: if (codeGenerationPrompt && selectedWidgetId && promptLoadedForWidgetId === s
|
|
|
14272
14732
|
{/if}
|
|
14273
14733
|
</svelte:fragment>
|
|
14274
14734
|
<svelte:fragment slot="version-history">
|
|
14275
|
-
|
|
14276
|
-
|
|
14277
|
-
|
|
14735
|
+
<!--
|
|
14736
|
+
A widget that has only ever been generated (never published) still
|
|
14737
|
+
has versions to preview — the history holds them as Drafts. Gating
|
|
14738
|
+
purely on `panelPublishedVersion` hid the picker for exactly those
|
|
14739
|
+
widgets, which is why the dropdown looked missing.
|
|
14740
|
+
-->
|
|
14741
|
+
{#if panelPublishedVersion !== null || ps.publishHistory.length > 0}
|
|
14742
|
+
{@const newestDraft = ps.publishHistory.find(
|
|
14743
|
+
(row) => !(row.publishedAt ?? row.published_at),
|
|
14744
|
+
)}
|
|
14745
|
+
{@const defaultVersionSource = panelPublishedVersion !== null
|
|
14746
|
+
? `v:${panelPublishedVersion}`
|
|
14747
|
+
: (newestDraft ? `v:${newestDraft.version}` : null)}
|
|
14748
|
+
{@const shapeSource = shapePreviewSourceByWidgetId[panelWidgetId] || defaultVersionSource}
|
|
14749
|
+
{@const viewingVersion = shapeSource && shapeSource.startsWith('v:')
|
|
14278
14750
|
? parseInt(shapeSource.slice(2), 10)
|
|
14279
|
-
:
|
|
14280
|
-
{@const viewingIsLive = Number(viewingVersion) === Number(
|
|
14751
|
+
: panelPublishedVersion}
|
|
14752
|
+
{@const viewingIsLive = Number(viewingVersion) === Number(panelPublishedVersion)}
|
|
14753
|
+
<!--
|
|
14754
|
+
A version is a Draft until it is published. The history is one
|
|
14755
|
+
sequence (`v1, v2, v3…`), so "draft" is read from the record's
|
|
14756
|
+
`publishedAt`, not from any second numbering scheme.
|
|
14757
|
+
-->
|
|
14758
|
+
{@const viewingRecord = ps.publishHistory.find(
|
|
14759
|
+
(row) => Number(row.version) === Number(viewingVersion),
|
|
14760
|
+
)}
|
|
14761
|
+
{@const viewingIsDraft = viewingRecord
|
|
14762
|
+
? !(viewingRecord.publishedAt ?? viewingRecord.published_at)
|
|
14763
|
+
: false}
|
|
14281
14764
|
<div class="wc-version-history-ct relative shrink-0 flex items-center gap-0.5">
|
|
14282
|
-
<HoverTooltip text="
|
|
14765
|
+
<HoverTooltip text="Switch version" maxWidth={120}>
|
|
14283
14766
|
<button
|
|
14284
14767
|
type="button"
|
|
14285
14768
|
onclick={(e) => {
|
|
@@ -14289,11 +14772,11 @@ $: if (codeGenerationPrompt && selectedWidgetId && promptLoadedForWidgetId === s
|
|
|
14289
14772
|
onmousedown={(e) => e.stopPropagation()}
|
|
14290
14773
|
class="wc-version-history-toggle flex items-center gap-0.5 text-[10px] font-semibold tabular-nums px-1.5 py-0.5 rounded border border-emerald-300 bg-emerald-50 text-emerald-800 hover:bg-emerald-100 cursor-pointer whitespace-nowrap"
|
|
14291
14774
|
>
|
|
14292
|
-
v{Number.isFinite(viewingVersion) ? viewingVersion :
|
|
14775
|
+
{`v${Number.isFinite(viewingVersion) ? viewingVersion : panelPublishedVersion}`}
|
|
14293
14776
|
<span class="text-emerald-500">{isHistoryOpen && isPanelFocused ? '▲' : '▼'}</span>
|
|
14294
14777
|
</button>
|
|
14295
14778
|
</HoverTooltip>
|
|
14296
|
-
{#if panelHasUnpublishedChanges}
|
|
14779
|
+
{#if panelHasUnpublishedChanges && !Number.isFinite(viewingVersion)}
|
|
14297
14780
|
<button
|
|
14298
14781
|
type="button"
|
|
14299
14782
|
onclick={(e) => {
|
|
@@ -14312,6 +14795,24 @@ $: if (codeGenerationPrompt && selectedWidgetId && promptLoadedForWidgetId === s
|
|
|
14312
14795
|
class="wc-version-live-btn shrink-0 inline-flex items-center justify-center min-w-[4.75rem] text-[10px] font-semibold uppercase tracking-wide px-1.5 py-0.5 rounded bg-emerald-100 text-emerald-600 opacity-80"
|
|
14313
14796
|
title="Live — generate new code to Publish a new version"
|
|
14314
14797
|
>Live</span>
|
|
14798
|
+
{:else if viewingIsDraft}
|
|
14799
|
+
{#if viewingIsDraft}
|
|
14800
|
+
<button
|
|
14801
|
+
type="button"
|
|
14802
|
+
onclick={(e) => {
|
|
14803
|
+
e.stopPropagation();
|
|
14804
|
+
if (!isPanelFocused) focusWidgetPanel(panelWidgetId);
|
|
14805
|
+
if (getPublishGateForPanel(panelWidgetId, Number(viewingVersion)).disabled) return;
|
|
14806
|
+
handleWidgetShapeVersionChange(panelWidgetId, `v:${viewingVersion}`, false);
|
|
14807
|
+
runPublishForPanel(panelWidgetId, Number(viewingVersion));
|
|
14808
|
+
}}
|
|
14809
|
+
disabled={!canPublishDraftVersionForPanel(panelWidgetId, viewingVersion) || getPublishGateForPanel(panelWidgetId, Number(viewingVersion)).disabled}
|
|
14810
|
+
title="Build this draft version and make it the Live version"
|
|
14811
|
+
class="wc-version-publish-btn shrink-0 inline-flex items-center justify-center min-w-[4.75rem] text-[10px] font-semibold uppercase tracking-wide px-1.5 py-0.5 rounded border border-emerald-600 bg-emerald-600 text-white hover:bg-emerald-700 disabled:opacity-50 disabled:cursor-not-allowed cursor-pointer"
|
|
14812
|
+
>
|
|
14813
|
+
{isPublishing && publishingWidgetId === panelWidgetId ? '...' : 'Publish'}
|
|
14814
|
+
</button>
|
|
14815
|
+
{/if}
|
|
14315
14816
|
{:else}
|
|
14316
14817
|
<button
|
|
14317
14818
|
type="button"
|
|
@@ -14338,60 +14839,44 @@ $: if (codeGenerationPrompt && selectedWidgetId && promptLoadedForWidgetId === s
|
|
|
14338
14839
|
onclick={() => { isHistoryOpen = false; }}
|
|
14339
14840
|
></button>
|
|
14340
14841
|
<div
|
|
14341
|
-
class="wc-version-history-panel flex flex-col gap-1
|
|
14842
|
+
class="wc-version-history-panel flex flex-col gap-1 rounded-md border border-emerald-200 bg-white shadow-lg p-1.5 z-[9999] w-max"
|
|
14342
14843
|
style="position: fixed; top: {liveHistoryPanelTop}px; right: {liveHistoryPanelRight}px; max-width: min(22rem, calc(100vw - 16px));"
|
|
14343
14844
|
>
|
|
14344
14845
|
{#if isLoadingHistory}
|
|
14345
14846
|
<p class="text-xs text-emerald-500 animate-pulse text-center py-2">Loading history...</p>
|
|
14346
14847
|
{:else if ps.publishHistory.length === 0}
|
|
14347
|
-
<p class="text-xs text-emerald-400 text-center py-2">No history records yet.
|
|
14848
|
+
<p class="text-xs text-emerald-400 text-center py-2">No history records yet. Generate or publish to start tracking.</p>
|
|
14348
14849
|
{:else}
|
|
14349
14850
|
{#each ps.publishHistory as record (record.id)}
|
|
14350
|
-
{@const
|
|
14351
|
-
{@const
|
|
14352
|
-
|
|
14353
|
-
|
|
14354
|
-
|
|
14355
|
-
|
|
14356
|
-
|
|
14357
|
-
|
|
14358
|
-
|
|
14359
|
-
|
|
14851
|
+
{@const rowVersion = Number(record.version)}
|
|
14852
|
+
{@const rowIsDraft = !(record.publishedAt ?? record.published_at)}
|
|
14853
|
+
{@const rowIsViewing = rowVersion === Number(viewingVersion)}
|
|
14854
|
+
{@const rowIsLive = !rowIsDraft && rowVersion === Number(panelPublishedVersion)}
|
|
14855
|
+
{@const rowDateLabel = formatPublishHistoryDate(record)}
|
|
14856
|
+
{@const publishGate = getPublishGateForPanel(panelWidgetId, rowVersion)}
|
|
14857
|
+
<VersionDropdownRow
|
|
14858
|
+
versionLabel={'v' + rowVersion}
|
|
14859
|
+
commitSha={record.commitSha ?? record.commit_sha ?? null}
|
|
14860
|
+
showCommit={debugMode}
|
|
14861
|
+
dateLabel={rowDateLabel}
|
|
14862
|
+
isLive={rowIsLive}
|
|
14863
|
+
isDraft={rowIsDraft}
|
|
14864
|
+
selected={rowIsViewing}
|
|
14865
|
+
busy={isPublishing && publishingWidgetId === panelWidgetId}
|
|
14866
|
+
publishDisabled={!canPublishDraftVersionForPanel(panelWidgetId, rowVersion) || publishGate.disabled}
|
|
14867
|
+
publishDisabledReason={publishGate.disabled ? publishGate.reason : null}
|
|
14868
|
+
setLiveDisabled={isSettingLiveVersion === record.version}
|
|
14869
|
+
onselect={() => {
|
|
14360
14870
|
handleWidgetShapeVersionChange(panelWidgetId, `v:${record.version}`);
|
|
14361
14871
|
}}
|
|
14362
|
-
|
|
14363
|
-
|
|
14364
|
-
|
|
14365
|
-
handleWidgetShapeVersionChange(panelWidgetId, `v:${record.version}`);
|
|
14366
|
-
}
|
|
14872
|
+
onpublish={() => {
|
|
14873
|
+
handleWidgetShapeVersionChange(panelWidgetId, `v:${rowVersion}`, false);
|
|
14874
|
+
runPublishForPanel(panelWidgetId, rowVersion);
|
|
14367
14875
|
}}
|
|
14368
|
-
|
|
14369
|
-
|
|
14370
|
-
|
|
14371
|
-
|
|
14372
|
-
{(record.commitSha ?? record.commit_sha).slice(0, 7)}
|
|
14373
|
-
</span>
|
|
14374
|
-
{/if}
|
|
14375
|
-
<span class="wc-live-history-date text-xs text-slate-400 shrink-0 whitespace-nowrap tabular-nums" style="padding-right: 4px;">
|
|
14376
|
-
{formatPublishHistoryDate(record)}
|
|
14377
|
-
</span>
|
|
14378
|
-
{#if !rowIsLive}
|
|
14379
|
-
<button
|
|
14380
|
-
type="button"
|
|
14381
|
-
onclick={(e) => {
|
|
14382
|
-
e.stopPropagation();
|
|
14383
|
-
void setLiveVersionForWidget(panelWidgetId, record.version);
|
|
14384
|
-
}}
|
|
14385
|
-
disabled={isSettingLiveVersion === record.version}
|
|
14386
|
-
class="shrink-0 inline-flex items-center justify-center min-w-[4.75rem] text-xs px-1.5 py-0.5 rounded border border-emerald-300 text-emerald-700 hover:bg-emerald-100 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
|
|
14387
|
-
title="Publish this version so visitors see it on your website"
|
|
14388
|
-
>
|
|
14389
|
-
{isSettingLiveVersion === record.version ? '...' : 'Set live'}
|
|
14390
|
-
</button>
|
|
14391
|
-
{:else}
|
|
14392
|
-
<span class="shrink-0 inline-flex items-center justify-center min-w-[4.75rem] text-xs px-1.5 py-0.5 rounded bg-emerald-100 text-emerald-600 font-medium">live</span>
|
|
14393
|
-
{/if}
|
|
14394
|
-
</div>
|
|
14876
|
+
onsetlive={() => {
|
|
14877
|
+
void setLiveVersionForWidget(panelWidgetId, record.version);
|
|
14878
|
+
}}
|
|
14879
|
+
/>
|
|
14395
14880
|
{/each}
|
|
14396
14881
|
{/if}
|
|
14397
14882
|
</div>
|