diff --git a/CHANGELOG.md b/CHANGELOG.md index 7df4ff8..013901f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,26 @@ +## 2.2.0 + +### Improvement: show the file path under the name in the IDE explorer + +Pages and scripts in the IDE file tree now show their **path** (`s3key`) in a muted second line under the file name. Same-named files (common for pages) are now distinguishable at a glance without having to open each one. The full path is also available as a hover tooltip. + +### Feature: "Republish all pages" + per-page republish result (KYTE-#181) + +Pairs with kyte-php v4.8.1, which makes the republish hook fault-isolated and returns a `republish_summary`. On the app **configuration page**: + +- A **"Republish all pages"** button (on the Authentication Mode card) — an explicit recovery action that re-stamps and re-deploys every published page with the current Kyte Connect code, then reports the result. Useful when an auto-republish half-completed. +- The **per-page result is now surfaced**: the auth-mode flip / kyte-connect update and the new button all read `republish_summary` and show how many pages succeeded/failed (failures are listed and logged to the console) — instead of a blind "it worked" toast. + +### Feature: Publish action in the IDE (KYTE-#189) + +The in-app IDE could only Save (which persists content); there was no way to publish from it. Added a **Publish** action for publishable file types (**pages** and **scripts** — the ones that deploy to S3): + +- A green **rocket** button in the IDE tab bar, shown only when the active file is a page or script. +- **Ctrl+Shift+S** keyboard shortcut (Save remains Ctrl+S); added to the welcome-screen shortcut list. +- Publishing sends `state=1` alongside the content, so the backend runs its normal publish path (page → `publishPage` → S3 + CloudFront; script → `handleScriptPublication` → S3) and creates a version with the change summary. Unlike Save, Publish is allowed even when the file isn't dirty (re-deploy current content), and it persists any pending edits in the same request. + +Pairs with kyte-php v4.8.1 (`block_layout` partial-save guard). The "IDE save didn't persist" issue from #189 was already fixed by kyte-php v4.8.0. + ## 2.1.0 ### Change: remove JavaScript obfuscation (pairs with kyte-php v4.7.0 — KYTE-#191) diff --git a/app/configuration.html b/app/configuration.html index e257593..ff4d15a 100644 --- a/app/configuration.html +++ b/app/configuration.html @@ -376,8 +376,12 @@

JWT Bearer (Recommended — rotating tokens) - -
+ +
+
+
+ Ctrl+Shift+S + Publish current file (pages & scripts) +
Ctrl+W Close current tab diff --git a/assets/css/kyte-ide.css b/assets/css/kyte-ide.css index 74c3602..f544608 100644 --- a/assets/css/kyte-ide.css +++ b/assets/css/kyte-ide.css @@ -261,6 +261,23 @@ body, html { text-overflow: ellipsis; } +/* Stacked name + path (pages & scripts) so same-named files are distinguishable */ +.tree-item .file-meta { + display: flex; + flex-direction: column; + flex: 1 1 auto; + min-width: 0; + overflow: hidden; +} + +.tree-item .file-path { + font-size: 0.68rem; + color: #6a6a6a; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + .tree-item .dirty-indicator { margin-left: auto; width: 8px; diff --git a/assets/js/source/kyte-shipyard-application-configuration.js b/assets/js/source/kyte-shipyard-application-configuration.js index 2c9d3e8..89e8465 100644 --- a/assets/js/source/kyte-shipyard-application-configuration.js +++ b/assets/js/source/kyte-shipyard-application-configuration.js @@ -250,6 +250,12 @@ function setupFormEventHandlers(_ks, idx) { e.preventDefault(); saveAuthModeSettings(_ks, idx); }); + + // Republish all pages handler (KYTE-#181) + $("#republishAllPages").click(function(e) { + e.preventDefault(); + republishAllPages(_ks, idx); + }); } // Handle user model change @@ -633,13 +639,21 @@ function updateKyteConnectCode(_ks) { // Update local app object app.kyte_connect = systemKyteCode; app.kyte_connect_obfuscated = ''; - + // Update current code and refresh comparison currentKyteCode = systemKyteCode; displayKyteCodes(); compareKyteCodes(); - - showSuccess('Kyte Connect code updated successfully!'); + + // Surface the per-page republish result (KYTE-#181) + const formatted = formatRepublishSummary(r.data[0] && r.data[0].republish_summary); + if (formatted && !formatted.ok) { + showError('Kyte Connect updated, but some pages failed to republish. ' + formatted.message); + } else if (formatted) { + showSuccess('Kyte Connect code updated. ' + formatted.message); + } else { + showSuccess('Kyte Connect code updated successfully!'); + } } else { showError('Failed to update Kyte Connect code. Please try again.'); } @@ -653,6 +667,46 @@ function updateKyteConnectCode(_ks) { }); } +// Format the backend's republish_summary (KYTE-#181) into a user-facing result. +// Returns { ok: bool, message: string } or null when no summary is present. +function formatRepublishSummary(summary) { + if (!summary) return null; + const ok = summary.succeeded || 0; + const failed = summary.failed || 0; + const total = ok + failed; + if (failed === 0) { + return { ok: true, message: `Republished ${ok} page${ok === 1 ? '' : 's'}.` }; + } + console.warn('Republish failures:', summary.failures); + const detail = (summary.failures || []) + .map(f => `page ${f.page}${f.s3key ? ' (' + f.s3key + ')' : ''}`) + .join(', '); + return { ok: false, message: `Republished ${ok} of ${total} pages — ${failed} failed: ${detail}. See console for details.` }; +} + +// Explicitly re-stamp and re-deploy every published page with the CURRENT Kyte +// Connect code, and report a per-page result. Recovery action for a half-completed +// auto-republish. See KYTE-#181. +function republishAllPages(_ks, idx) { + const btn = $("#republishAllPages"); + const original = btn.html(); + btn.html('Republishing...').prop('disabled', true); + + _ks.put('Application', 'id', idx, { 'republish_kyte_connect': 1 }, null, [], function(r) { + const summary = (r.data && r.data[0]) ? r.data[0].republish_summary : null; + const formatted = formatRepublishSummary(summary); + if (formatted && !formatted.ok) { + showError(formatted.message); + } else { + showSuccess(formatted ? formatted.message : 'All pages republished.'); + } + btn.html(original).prop('disabled', false); + }, function(err) { + showError('Republish failed: ' + err); + btn.html(original).prop('disabled', false); + }); +} + // Show detailed diff in a modal function showDetailedDiff() { const modal = document.createElement('div'); diff --git a/assets/js/source/kyte-shipyard-ide.js b/assets/js/source/kyte-shipyard-ide.js index 8a74719..a5ff9c0 100644 --- a/assets/js/source/kyte-shipyard-ide.js +++ b/assets/js/source/kyte-shipyard-ide.js @@ -65,6 +65,7 @@ const FILE_TYPES = { iconClass: 'html', sectionIcon: 'fas fa-sitemap', model: 'KytePage', + publishable: true, // can be published to S3 (state=1 → publishPage) loadModel: 'KytePageData', // Special loader: uses KytePageData to get content loadField: 'page', // KytePageData is keyed by 'page' field listField: 'site', @@ -119,6 +120,7 @@ const FILE_TYPES = { iconClass: 'js', sectionIcon: 'fas fa-scroll', model: 'KyteScript', + publishable: true, // can be published to S3 (state=1 → handleScriptPublication) listField: 'site', parts: [ { key: 'content', label: 'JavaScript', language: 'javascript', field: 'content' } @@ -405,6 +407,7 @@ function renderPagesSection() { html += renderGroup(site.name || 'Unnamed Site', 'fas fa-globe', site._pages.map(page => ({ fileId: generateFileId(FILE_TYPES.PAGE, page.id), name: FILE_TYPES.PAGE.displayName(page), + path: page.s3key || '', iconClass: FILE_TYPES.PAGE.iconClass, icon: FILE_TYPES.PAGE.icon }))); @@ -437,6 +440,7 @@ function renderScriptsSection() { return { fileId: generateFileId(FILE_TYPES.SCRIPT, script.id), name: FILE_TYPES.SCRIPT.displayName(script), + path: script.s3key || '', iconClass: isCss ? 'css' : 'js', icon: isCss ? 'fab fa-css3-alt' : 'fab fa-js' }; @@ -473,9 +477,12 @@ function renderGroup(label, icon, items) {
${items.map(item => ` -
+
- ${escapeHtml(item.name)} + + ${escapeHtml(item.name)} + ${item.path ? `${escapeHtml(item.path)}` : ''} +
`).join('')} @@ -690,7 +697,16 @@ function renderTabs() { `; - tabBar.innerHTML = tabsHtml + historyBtnHtml; + // Show a Publish button only for publishable file types (pages, scripts → S3) + const isPublishable = activeTab && activeTab.fileType.publishable; + const publishBtnHtml = ` + + `; + + tabBar.innerHTML = tabsHtml + publishBtnHtml + historyBtnHtml; } function renderSubTabs(tab) { @@ -872,7 +888,7 @@ function saveFile(fileId, callback) { } } -function performSave(fileId, fileType, itemId, buf, changeSummary, callback) { +function performSave(fileId, fileType, itemId, buf, changeSummary, callback, publish) { // Build update data const data = {}; fileType.parts.forEach(part => { @@ -884,13 +900,19 @@ function performSave(fileId, fileType, itemId, buf, changeSummary, callback) { data.change_summary = changeSummary; } + // Publish: state=1 triggers the backend publish (page → S3 via publishPage, + // script → S3 via handleScriptPublication). Save without it just persists. + if (publish) { + data.state = 1; + } + // For Pages, the save target is KytePage and the ID is from the nested page object const saveModel = fileType.model; const saveId = (fileType.id === 'page' && buf.item && buf.item.page) ? buf.item.page.id : itemId; - notify('info', 'Saving...'); + notify('info', publish ? 'Publishing...' : 'Saving...'); _ks.put(saveModel, 'id', saveId, data, null, [], function(r) { // Update original to match current (no longer dirty) @@ -900,7 +922,7 @@ function performSave(fileId, fileType, itemId, buf, changeSummary, callback) { updateDirtyIndicators(fileId); renderTabs(); - notify('success', 'Saved successfully'); + notify('success', publish ? 'Published successfully' : 'Saved successfully'); // Refresh version history if panel is open if (historyPanelOpen && activeTabId === fileId) { @@ -909,7 +931,7 @@ function performSave(fileId, fileType, itemId, buf, changeSummary, callback) { if (callback) callback(); }, function(err) { - notify('error', 'Save failed: ' + err); + notify('error', (publish ? 'Publish failed: ' : 'Save failed: ') + err); }); } @@ -919,6 +941,45 @@ function saveActiveFile() { } } +// Publish the active file to its live site (pages/scripts → S3). Unlike save, +// publishing is allowed even when not dirty (re-deploy current content). It also +// persists any pending edits in the same request (state=1 + content). See KYTE-#189. +function publishActiveFile() { + if (activeTabId) { + publishFile(activeTabId); + } +} + +function publishFile(fileId, callback) { + const buf = fileBuffers[fileId]; + if (!buf) return; + + const { fileType, itemId } = parseFileId(fileId); + + if (!fileType.publishable) { + notify('info', 'This file type deploys automatically on save — no separate publish.'); + return; + } + + // Flush current editor content to buffer + if (activeTabId === fileId) { + saveEditorToBuffer(); + } + + // Versioned types capture a change summary (publish also creates a version) + if (fileType.versioning) { + showChangeSummaryModal(function(action, summary) { + if (action === 'cancel') { + return; + } + const changeSummary = action === 'save' ? summary : ''; + performSave(fileId, fileType, itemId, buf, changeSummary, callback, true); + }); + } else { + performSave(fileId, fileType, itemId, buf, null, callback, true); + } +} + // ============================================ // Change Summary Modal // ============================================ @@ -1196,8 +1257,15 @@ function initKeyboardShortcuts() { document.addEventListener('keydown', function(e) { const isCtrl = e.ctrlKey || e.metaKey; + // Ctrl+Shift+S — Publish + if (isCtrl && e.shiftKey && (e.key === 's' || e.key === 'S')) { + e.preventDefault(); + publishActiveFile(); + return; + } + // Ctrl+S — Save - if (isCtrl && e.key === 's') { + if (isCtrl && !e.shiftKey && e.key === 's') { e.preventDefault(); saveActiveFile(); } @@ -1345,6 +1413,7 @@ window.kyteIDE = { toggleSection, toggleGroup, saveActiveFile, + publishActiveFile, collapseAll, toggleHistory, restoreVersion diff --git a/assets/js/source/kyte-shipyard.js b/assets/js/source/kyte-shipyard.js index f9529cb..520b869 100644 --- a/assets/js/source/kyte-shipyard.js +++ b/assets/js/source/kyte-shipyard.js @@ -1,4 +1,4 @@ -var KS_VERSION = '2.1.0'; +var KS_VERSION = '2.2.0'; function loadScript(url, callback) { var script = document.createElement('script'); diff --git a/package.json b/package.json index 810ebe1..9d8ba05 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "kyte-shipyard", - "version": "2.1.0", + "version": "2.2.0", "private": true, "description": "Kyte Shipyard — admin UI for the Kyte platform. Sources in assets/js/source/, esbuild minifies to assets/js/ (gitignored).", "type": "module",