Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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)
Expand Down
8 changes: 6 additions & 2 deletions app/configuration.html
Original file line number Diff line number Diff line change
Expand Up @@ -376,8 +376,12 @@ <h3 class="config-card-title" data-i18n="ui.config.kyte_connect.auth_mode.title"
<option value="jwt" data-i18n="ui.config.kyte_connect.auth_mode.jwt">JWT Bearer (Recommended — rotating tokens)</option>
</select>
</div>
<!-- Save Button -->
<div class="d-flex justify-content-end">
<!-- Action Buttons -->
<div class="d-flex justify-content-between align-items-center">
<button id="republishAllPages" class="btn btn-outline-secondary" title="Re-stamp and re-deploy every published page with the current Kyte Connect code">
<i class="fas fa-sync-alt me-2"></i>
<span>Republish all pages</span>
</button>
<button id="saveAuthModeSettings" class="btn btn-primary">
<i class="fas fa-save me-2"></i>
<span data-i18n="ui.config.kyte_connect.auth_mode.save_button">Save Authentication Mode</span>
Expand Down
4 changes: 4 additions & 0 deletions app/ide/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,10 @@ <h2>Kyte IDE</h2>
<span class="shortcut-key">Ctrl+S</span>
<span>Save current file</span>
</div>
<div class="shortcut-item">
<span class="shortcut-key">Ctrl+Shift+S</span>
<span>Publish current file (pages &amp; scripts)</span>
</div>
<div class="shortcut-item">
<span class="shortcut-key">Ctrl+W</span>
<span>Close current tab</span>
Expand Down
17 changes: 17 additions & 0 deletions assets/css/kyte-ide.css
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
60 changes: 57 additions & 3 deletions assets/js/source/kyte-shipyard-application-configuration.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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.');
}
Expand All @@ -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('<i class="fas fa-spinner fa-spin me-2"></i>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');
Expand Down
85 changes: 77 additions & 8 deletions assets/js/source/kyte-shipyard-ide.js
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down Expand Up @@ -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' }
Expand Down Expand Up @@ -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
})));
Expand Down Expand Up @@ -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'
};
Expand Down Expand Up @@ -473,9 +477,12 @@ function renderGroup(label, icon, items) {
</div>
<div class="tree-group-items">
${items.map(item => `
<div class="tree-item nested" data-file-id="${item.fileId}" onclick="window.kyteIDE.openFile('${item.fileId}')">
<div class="tree-item nested" data-file-id="${item.fileId}" onclick="window.kyteIDE.openFile('${item.fileId}')" title="${escapeHtml(item.path || item.name)}">
<i class="file-icon ${item.iconClass} ${item.icon}"></i>
<span class="file-name">${escapeHtml(item.name)}</span>
<span class="file-meta">
<span class="file-name">${escapeHtml(item.name)}</span>
${item.path ? `<span class="file-path">${escapeHtml(item.path)}</span>` : ''}
</span>
<span class="dirty-indicator"></span>
</div>
`).join('')}
Expand Down Expand Up @@ -690,7 +697,16 @@ function renderTabs() {
</button>
`;

tabBar.innerHTML = tabsHtml + historyBtnHtml;
// Show a Publish button only for publishable file types (pages, scripts → S3)
const isPublishable = activeTab && activeTab.fileType.publishable;
const publishBtnHtml = `
<button class="history-toggle-btn ide-publish-btn ${isPublishable ? '' : 'hidden'}"
onclick="window.kyteIDE.publishActiveFile()" title="Publish to live site (Ctrl+Shift+S)">
<i class="fas fa-rocket" style="color:#34c759;"></i>
</button>
`;

tabBar.innerHTML = tabsHtml + publishBtnHtml + historyBtnHtml;
}

function renderSubTabs(tab) {
Expand Down Expand Up @@ -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 => {
Expand All @@ -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)
Expand All @@ -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) {
Expand All @@ -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);
});
}

Expand All @@ -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
// ============================================
Expand Down Expand Up @@ -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();
}
Expand Down Expand Up @@ -1345,6 +1413,7 @@ window.kyteIDE = {
toggleSection,
toggleGroup,
saveActiveFile,
publishActiveFile,
collapseAll,
toggleHistory,
restoreVersion
Expand Down
2 changes: 1 addition & 1 deletion assets/js/source/kyte-shipyard.js
Original file line number Diff line number Diff line change
@@ -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');
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down