Loading editor/html/index.html +16 −10 Changes for editor/html/index.html: 16 added lines, 10 removed lines. Original line number Diff line number Diff line Loading @@ -45,9 +45,15 @@ <button id="btn-export-xml" data-i18n-title="I18N_TITLE_EXPORT_XML">⬆ <span data-i18n="I18N_EXPORT_XML">Export XML</span></button> <button id="btn-import-html" data-i18n-title="I18N_IMPORT_HTML">⬇ <span data-i18n="I18N_IMPORT_HTML">Import HTML</span></button> <button id="btn-export-html" data-i18n-title="I18N_EXPORT_HTML">⬆ <span data-i18n="I18N_EXPORT_HTML">Export HTML</span></button> <button id="btn-conn-manager" data-i18n-title="I18N_CONNECTIONS">🔗 <span data-i18n="I18N_CONNECTIONS">Verbindungen</span></button> <button id="btn-conn-manager" data-i18n-title="I18N_PROJECTS">🔗 <span data-i18n="I18N_PROJECTS">Projekte</span></button> <button id="btn-survey-manager" title="Umfragen verwalten">📋 <span>Umfragen</span></button> </div> <div class="toolbar-group"> <select id="project-select" title="Projekt auswählen" style="padding:0.2em 0.4em;border-radius:4px;border:1px solid var(--border);background:var(--bg-secondary);color:var(--text-primary);font-size:0.8rem;cursor:pointer"> <option value="" data-i18n="I18N_ALL_PROJECTS">Alle Projekte</option> </select> <span id="project-status" title="Verbindungsstatus" style="font-size:0.8rem"></span> </div> <div class="toolbar-group"> <button id="btn-ai" data-i18n-title="I18N_AI_TITLE">🤖 <span data-i18n="I18N_AI">KI</span></button> </div> Loading Loading @@ -130,7 +136,7 @@ <dialog id="save-dialog"> <h3 id="save-dialog-title" data-i18n="I18N_SAVE_TPL_TITLE">Dokument speichern</h3> <label><span data-i18n="I18N_GROUP">Gruppe</span>: <input type="text" id="save-group" data-i18n-placeholder="I18N_GROUP_PLACEHOLDER" placeholder="z.B. Vorlagen/Blog"></label> <label><span data-i18n="I18N_PROJECT">Projekt</span>: <select id="save-group"></select></label> <label><span data-i18n="I18N_TPL_NAME_LABEL">Name</span>: <input type="text" id="save-name" data-i18n-placeholder="I18N_TPL_NAME_PLACEHOLDER" placeholder="Dokumentname"></label> <div class="set-field" style="display:flex;gap:0.5em;flex-wrap:wrap"> <button id="btn-doc-seo" type="button" class="set-save-btn" style="background:var(--bg-tertiary)">🔍 <span data-i18n="I18N_SEO_SOCIAL">SEO & Social…</span></button> Loading Loading @@ -253,13 +259,13 @@ <dialog id="connection-manager-dialog" class="settings-dialog"> <div class="set-modal"> <div class="set-sidebar"> <h3 data-i18n="I18N_CONNECTIONS">Verbindungen</h3> <h3 data-i18n="I18N_PROJECTS">Projekte</h3> <div id="conn-list" class="conn-list"></div> <button id="btn-conn-add" class="set-save-btn" style="margin-top:0.5em;width:100%">+ <span data-i18n="I18N_ADD">Hinzufügen</span></button> </div> <div class="set-content"> <div id="conn-form" style="display:none"> <h4 id="conn-form-title" data-i18n="I18N_CONNECTION">Verbindung</h4> <h4 id="conn-form-title" data-i18n="I18N_PROJECT">Projekt</h4> <input type="hidden" id="conn-edit-id" value=""> <div class="set-field"> <label data-i18n="I18N_CONN_GROUP">Gruppe</label> Loading Loading @@ -297,7 +303,7 @@ <span id="conn-status" class="set-status"></span> </div> <div id="conn-placeholder"> <p class="hint" data-i18n="I18N_SELECT_CONNECTION">Verbindung auswählen oder neue hinzufügen.</p> <p class="hint" data-i18n="I18N_SELECT_PROJECT">Projekt auswählen oder neues hinzufügen.</p> </div> </div> </div> Loading Loading @@ -329,7 +335,7 @@ </div> </div> <div class="dialog-actions"> <button id="btn-publish-settings" data-i18n="Settings" style="margin-right:auto">⚙ <span data-i18n="I18N_CONNECTIONS">Verbindungen</span></button> <button id="btn-publish-settings" data-i18n="Settings" style="margin-right:auto">⚙ <span data-i18n="I18N_PROJECTS">Projekte</span></button> <button id="btn-publish-close" data-i18n="Close">Schließen</button> </div> </dialog> Loading Loading @@ -404,7 +410,7 @@ <ul class="set-nav"> <li class="set-nav-item active" data-section="language" data-i18n="Language">Sprache</li> <li class="set-nav-item" data-section="ai" data-i18n="I18N_AI">KI</li> <li class="set-nav-item" data-section="connection" data-i18n="I18N_CONNECTION">Verbindung</li> <li class="set-nav-item" data-section="connection" data-i18n="I18N_PROJECT">Projekt</li> <li class="set-nav-item" data-section="mcp" data-i18n="I18N_MCP">MCP</li> </ul> </div> Loading Loading @@ -452,10 +458,10 @@ <!-- Connection section --> <div id="settings-connection" class="set-section" style="display:none"> <h4 data-i18n="I18N_CONNECTIONS">Verbindungen</h4> <p class="hint" data-i18n="I18N_CONN_MANAGER_HINT">Verbindungen werden im Verbindungsmanager verwaltet.</p> <h4 data-i18n="I18N_PROJECTS">Projekte</h4> <p class="hint" data-i18n="I18N_PROJECT_MANAGER_HINT">Projekte werden im Projekt-Manager verwaltet.</p> <div class="set-field"> <button id="btn-settings-open-connmgr" class="set-save-btn" data-i18n="I18N_OPEN_CONN_MANAGER">Verbindungsmanager öffnen</button> <button id="btn-settings-open-connmgr" class="set-save-btn" data-i18n="I18N_OPEN_PROJECT_MANAGER">Projekt-Manager öffnen</button> </div> </div> Loading editor/html/js/api.js +24 −8 Changes for editor/html/js/api.js: 24 added lines, 8 removed lines. Original line number Diff line number Diff line Loading @@ -95,6 +95,21 @@ var EditorApi = (function() { return { promise: promise, abort: function() { xhr.abort(); } }; } // Endpoints scoped to a connection/project (publish, media, survey) require a live // server-side session (_connSessions in webedit_api.cpp) that can be evicted (auth // expired, server restart, ...). Those handlers reply 403 "Not connected...". Rather // than surfacing that to the user, reconnect once and retry transparently. function requestWithReconnect(connId, method, url, data) { return request(method, url, data).catch(function(err) { if (err && typeof err.error === 'string' && err.error.indexOf('Not connected') === 0) { return request('POST', '/api/connection/connect/' + connId, {}).then(function() { return request(method, url, data); }); } return Promise.reject(err); }); } return { login: function(username, password) { return request('POST', '/api/login', { Loading Loading @@ -379,7 +394,7 @@ var EditorApi = (function() { }, publishToConnection: function(id, target, params) { return request('POST', '/api/connection/publish/' + id, { return requestWithReconnect(id, 'POST', '/api/connection/publish/' + id, { target: target, params: params || {} }); Loading @@ -387,19 +402,19 @@ var EditorApi = (function() { // Media browse (via blog connection) mediaListAlbums: function(connId) { return request('POST', '/api/connection/media/' + connId, { return requestWithReconnect(connId, 'POST', '/api/connection/media/' + connId, { command: 'media_list_albums' }); }, mediaListStores: function(connId) { return request('POST', '/api/connection/media/' + connId, { return requestWithReconnect(connId, 'POST', '/api/connection/media/' + connId, { command: 'media_list_stores' }); }, mediaCreateAlbum: function(connId, storeName, albumName, isPublic) { return request('POST', '/api/connection/media/' + connId, { return requestWithReconnect(connId, 'POST', '/api/connection/media/' + connId, { command: 'media_create_album', store_name: storeName, album_name: albumName, Loading @@ -408,7 +423,7 @@ var EditorApi = (function() { }, mediaUpload: function(connId, albumId, filename, dataBase64) { return request('POST', '/api/connection/media/' + connId, { return requestWithReconnect(connId, 'POST', '/api/connection/media/' + connId, { command: 'media_upload', album_id: albumId, filename: filename, Loading @@ -417,7 +432,7 @@ var EditorApi = (function() { }, mediaListMedia: function(connId, albumId) { return request('POST', '/api/connection/media/' + connId, { return requestWithReconnect(connId, 'POST', '/api/connection/media/' + connId, { command: 'media_list_media', album_id: albumId }); Loading @@ -430,7 +445,7 @@ var EditorApi = (function() { }; if (width > 0) params.width = width; if (height > 0) params.height = height; return request('POST', '/api/connection/media/' + connId, params); return requestWithReconnect(connId, 'POST', '/api/connection/media/' + connId, params); }, // HTML import/export Loading Loading @@ -471,7 +486,8 @@ var EditorApi = (function() { }, publishSurvey: function(surveyId, connId) { return request('POST', '/api/survey/publish', { survey_id: surveyId, conn_id: connId }); return requestWithReconnect(connId, 'POST', '/api/survey/publish', { survey_id: surveyId, conn_id: connId }); }, // Plugin settings (served by the backend settings router via AJAX) Loading editor/html/js/editor.js +152 −5 Changes for editor/html/js/editor.js: 152 added lines, 5 removed lines. Original line number Diff line number Diff line Loading @@ -10,6 +10,10 @@ var currentDocGroup = ''; var currentDocSeo = {}; var saveDialogMode = 'save'; var projectsCache = []; var currentProjectId = ''; var currentProjectName = ''; var CURRENT_PROJECT_STORAGE_KEY = 'blogi-current-project'; var wizardTemplates = []; var wizardCsvRows = []; var wizardCsvColumns = []; Loading @@ -33,6 +37,7 @@ try { SurveyManager.init(); } catch(e) { console.error('SurveyManager.init:', e); } try { Preview.bindToggle(); } catch(e) { console.error('Preview.bindToggle:', e); } try { Canvas.bindMessageListener(); } catch(e) { console.error('Canvas.bindMessageListener:', e); } try { initProjectSelector(); } catch(e) { console.error('initProjectSelector:', e); } Preview.clear(); // Selection can be driven either by clicking directly on an Loading Loading @@ -225,7 +230,11 @@ document.getElementById('btn-open').addEventListener('click', function() { EditorApi.listDocuments().then(function(resp) { renderDocList(resp.documents || []); var docs = resp.documents || []; if (currentProjectName) { docs = docs.filter(function(d) { return (d.group || '') === currentProjectName; }); } renderDocList(docs); document.getElementById('open-dialog').showModal(); }); }); Loading Loading @@ -365,10 +374,37 @@ I18n.t('I18N_SAVE_AS_TPL_TITLE', 'Dokument speichern unter') : I18n.t('I18N_SAVE_TPL_TITLE', 'Dokument speichern'); document.getElementById('save-name').value = name || ''; document.getElementById('save-group').value = group || ''; populateSaveGroupSelect(group || currentProjectName || ''); document.getElementById('save-dialog').showModal(); } // Populates the "Speichern unter" project select from projectsCache. `selectedGroup` // is preselected if it matches a known project name; otherwise (legacy free-text // groups from before projects existed) it's appended as an extra option so existing // documents don't silently lose their group on the next save. function populateSaveGroupSelect(selectedGroup) { var select = document.getElementById('save-group'); select.innerHTML = ''; var blankOpt = document.createElement('option'); blankOpt.value = ''; blankOpt.textContent = I18n.t('I18N_NO_PROJECT', 'Kein Projekt'); select.appendChild(blankOpt); appendGroupedProjectOptions(select, function(p) { return p.name; }); var hasSelected = !selectedGroup; var options = select.querySelectorAll('option'); for (var i = 0; i < options.length; i++) { if (options[i].value === selectedGroup) { hasSelected = true; break; } } if (selectedGroup && !hasSelected) { var extra = document.createElement('option'); extra.value = selectedGroup; extra.textContent = selectedGroup; select.appendChild(extra); } select.value = selectedGroup || ''; } // Fills the document-level SEO & Social dialog from currentDocSeo (see // DocumentState::seo / <WebEditor><SeoMeta> in webedit_api.cpp) — this // metadata travels with the saved template's XML, unlike the per-publish Loading Loading @@ -1665,6 +1701,111 @@ } } // --- Project selector (toolbar) --- // A "project" is the same underlying entity as a saved connection (name/group/url/ // ignore_ssl — see EditorApi.listConnections/connectToConnection and the project // manager dialog below). The toolbar select exposes it under project terminology: // picking one auto-connects and becomes the default group when saving documents. function initProjectSelector() { document.getElementById('project-select').addEventListener('change', function() { onProjectSelected(this.value); }); loadProjectSelectOptions(function() { var saved = ''; try { saved = localStorage.getItem(CURRENT_PROJECT_STORAGE_KEY) || ''; } catch(e) {} if (saved && projectsCache.some(function(p) { return p.id === saved; })) { document.getElementById('project-select').value = saved; onProjectSelected(saved); } }); } // Fetches the project list and (re)renders the toolbar select; shared with the // project manager dialog (loadConnectionList) so both stay in sync after add/edit/delete. function loadProjectSelectOptions(callback) { EditorApi.listConnections().then(function(resp) { projectsCache = resp.connections || []; renderProjectToolbarSelect(); if (callback) callback(); }); } function renderProjectToolbarSelect() { var select = document.getElementById('project-select'); select.innerHTML = '<option value="">' + I18n.t('I18N_ALL_PROJECTS', 'Alle Projekte') + '</option>'; appendGroupedProjectOptions(select, function(p) { return p.id; }); select.value = currentProjectId || ''; } // Builds <optgroup>-grouped <option>s (grouped like the project manager's sidebar // list) into `select` from projectsCache; valueFn picks the option value per project. function appendGroupedProjectOptions(select, valueFn) { var groups = {}; for (var i = 0; i < projectsCache.length; i++) { var g = projectsCache[i].group || ''; if (!groups[g]) groups[g] = []; groups[g].push(projectsCache[i]); } var groupNames = Object.keys(groups).sort(); for (var gi = 0; gi < groupNames.length; gi++) { var gName = groupNames[gi]; var container = select; if (gName) { container = document.createElement('optgroup'); container.label = gName; select.appendChild(container); } var projs = groups[gName]; for (var pi = 0; pi < projs.length; pi++) { var opt = document.createElement('option'); opt.value = valueFn(projs[pi]); opt.textContent = projs[pi].name; container.appendChild(opt); } } } function onProjectSelected(id) { var statusEl = document.getElementById('project-status'); if (!id) { currentProjectId = ''; currentProjectName = ''; try { localStorage.removeItem(CURRENT_PROJECT_STORAGE_KEY); } catch(e) {} statusEl.textContent = ''; statusEl.title = ''; return; } var proj = null; for (var i = 0; i < projectsCache.length; i++) { if (projectsCache[i].id === id) { proj = projectsCache[i]; break; } } currentProjectId = id; currentProjectName = proj ? proj.name : ''; try { localStorage.setItem(CURRENT_PROJECT_STORAGE_KEY, id); } catch(e) {} connectCurrentProject(); } // Auto-connect (and, via the "Not connected" retry in EditorApi, auto-reconnect on // the next connection-scoped call) whenever the current project changes or is // restored from localStorage on load. function connectCurrentProject() { if (!currentProjectId) return; var statusEl = document.getElementById('project-status'); statusEl.textContent = I18n.t('I18N_CONNECTING', 'Verbinde...'); statusEl.title = ''; EditorApi.connectToConnection(currentProjectId).then(function(resp) { connectedConnections[currentProjectId] = true; statusEl.textContent = '✓'; statusEl.title = I18n.t('I18N_CONNECTION_OK', 'Verbunden'); showPreviewPublishTargets(resp.targets || []); }).catch(function(err) { statusEl.textContent = '✗'; statusEl.title = I18n.t('I18N_CONNECTION_FAILED', 'Verbindung fehlgeschlagen') + ': ' + ((err && err.error) || ''); }); } // --- Connection Manager --- // Track which connections are currently "connected" (have authid on server) Loading Loading @@ -1738,6 +1879,12 @@ if (!confirm(I18n.t('I18N_REMOVE_CONFIRM', 'Wirklich l\u00f6schen') + '?')) return; EditorApi.deleteConnection(id).then(function() { delete connectedConnections[id]; if (id === currentProjectId) { currentProjectId = ''; currentProjectName = ''; try { localStorage.removeItem(CURRENT_PROJECT_STORAGE_KEY); } catch(e) {} document.getElementById('project-status').textContent = ''; } document.getElementById('conn-form').style.display = 'none'; document.getElementById('conn-placeholder').style.display = 'block'; loadConnectionList(); Loading Loading @@ -1958,8 +2105,8 @@ } function loadConnectionList() { EditorApi.listConnections().then(function(resp) { renderConnectionList(resp.connections || []); loadProjectSelectOptions(function() { renderConnectionList(projectsCache); }); } Loading Loading @@ -2023,7 +2170,7 @@ if (connections.length === 0) { list.innerHTML = '<p class="hint" style="padding:0.5em">' + I18n.t('I18N_NO_CONNECTIONS', 'Keine Verbindungen') + '</p>'; I18n.t('I18N_NO_PROJECTS', 'Keine Projekte') + '</p>'; } } Loading src/translations.h +18 −0 Changes for src/translations.h: 18 added lines, 0 removed lines. Original line number Diff line number Diff line Loading @@ -392,6 +392,15 @@ namespace blogi { {"I18N_NO_CONNECTIONS", "Keine Verbindungen"}, {"I18N_CONN_MANAGER_HINT", "Verbindungen werden im Verbindungsmanager verwaltet."}, {"I18N_OPEN_CONN_MANAGER", "Verbindungsmanager öffnen"}, // Projects (editor toolbar/dialog wording for the same connection entity) {"I18N_PROJECT", "Projekt"}, {"I18N_PROJECTS", "Projekte"}, {"I18N_SELECT_PROJECT", "Projekt auswählen oder neues hinzufügen."}, {"I18N_NO_PROJECTS", "Keine Projekte"}, {"I18N_NO_PROJECT", "Kein Projekt"}, {"I18N_ALL_PROJECTS", "Alle Projekte"}, {"I18N_PROJECT_MANAGER_HINT","Projekte werden im Projekt-Manager verwaltet."}, {"I18N_OPEN_PROJECT_MANAGER","Projekt-Manager öffnen"}, {"I18N_ADD", "Hinzufügen"}, {"I18N_DELETE", "Löschen"}, {"I18N_FIELDS_REQUIRED", "Name und URL sind erforderlich"}, Loading Loading @@ -855,6 +864,15 @@ namespace blogi { {"I18N_NO_CONNECTIONS", "No connections"}, {"I18N_CONN_MANAGER_HINT", "Connections are managed in the Connection Manager."}, {"I18N_OPEN_CONN_MANAGER", "Open Connection Manager"}, // Projects (editor toolbar/dialog wording for the same connection entity) {"I18N_PROJECT", "Project"}, {"I18N_PROJECTS", "Projects"}, {"I18N_SELECT_PROJECT", "Select a project or add a new one."}, {"I18N_NO_PROJECTS", "No projects"}, {"I18N_NO_PROJECT", "No project"}, {"I18N_ALL_PROJECTS", "All projects"}, {"I18N_PROJECT_MANAGER_HINT","Projects are managed in the Project Manager."}, {"I18N_OPEN_PROJECT_MANAGER","Open Project Manager"}, {"I18N_ADD", "Add"}, {"I18N_DELETE", "Delete"}, {"I18N_FIELDS_REQUIRED", "Name and URL are required"}, Loading Loading
editor/html/index.html +16 −10 Changes for editor/html/index.html: 16 added lines, 10 removed lines. Original line number Diff line number Diff line Loading @@ -45,9 +45,15 @@ <button id="btn-export-xml" data-i18n-title="I18N_TITLE_EXPORT_XML">⬆ <span data-i18n="I18N_EXPORT_XML">Export XML</span></button> <button id="btn-import-html" data-i18n-title="I18N_IMPORT_HTML">⬇ <span data-i18n="I18N_IMPORT_HTML">Import HTML</span></button> <button id="btn-export-html" data-i18n-title="I18N_EXPORT_HTML">⬆ <span data-i18n="I18N_EXPORT_HTML">Export HTML</span></button> <button id="btn-conn-manager" data-i18n-title="I18N_CONNECTIONS">🔗 <span data-i18n="I18N_CONNECTIONS">Verbindungen</span></button> <button id="btn-conn-manager" data-i18n-title="I18N_PROJECTS">🔗 <span data-i18n="I18N_PROJECTS">Projekte</span></button> <button id="btn-survey-manager" title="Umfragen verwalten">📋 <span>Umfragen</span></button> </div> <div class="toolbar-group"> <select id="project-select" title="Projekt auswählen" style="padding:0.2em 0.4em;border-radius:4px;border:1px solid var(--border);background:var(--bg-secondary);color:var(--text-primary);font-size:0.8rem;cursor:pointer"> <option value="" data-i18n="I18N_ALL_PROJECTS">Alle Projekte</option> </select> <span id="project-status" title="Verbindungsstatus" style="font-size:0.8rem"></span> </div> <div class="toolbar-group"> <button id="btn-ai" data-i18n-title="I18N_AI_TITLE">🤖 <span data-i18n="I18N_AI">KI</span></button> </div> Loading Loading @@ -130,7 +136,7 @@ <dialog id="save-dialog"> <h3 id="save-dialog-title" data-i18n="I18N_SAVE_TPL_TITLE">Dokument speichern</h3> <label><span data-i18n="I18N_GROUP">Gruppe</span>: <input type="text" id="save-group" data-i18n-placeholder="I18N_GROUP_PLACEHOLDER" placeholder="z.B. Vorlagen/Blog"></label> <label><span data-i18n="I18N_PROJECT">Projekt</span>: <select id="save-group"></select></label> <label><span data-i18n="I18N_TPL_NAME_LABEL">Name</span>: <input type="text" id="save-name" data-i18n-placeholder="I18N_TPL_NAME_PLACEHOLDER" placeholder="Dokumentname"></label> <div class="set-field" style="display:flex;gap:0.5em;flex-wrap:wrap"> <button id="btn-doc-seo" type="button" class="set-save-btn" style="background:var(--bg-tertiary)">🔍 <span data-i18n="I18N_SEO_SOCIAL">SEO & Social…</span></button> Loading Loading @@ -253,13 +259,13 @@ <dialog id="connection-manager-dialog" class="settings-dialog"> <div class="set-modal"> <div class="set-sidebar"> <h3 data-i18n="I18N_CONNECTIONS">Verbindungen</h3> <h3 data-i18n="I18N_PROJECTS">Projekte</h3> <div id="conn-list" class="conn-list"></div> <button id="btn-conn-add" class="set-save-btn" style="margin-top:0.5em;width:100%">+ <span data-i18n="I18N_ADD">Hinzufügen</span></button> </div> <div class="set-content"> <div id="conn-form" style="display:none"> <h4 id="conn-form-title" data-i18n="I18N_CONNECTION">Verbindung</h4> <h4 id="conn-form-title" data-i18n="I18N_PROJECT">Projekt</h4> <input type="hidden" id="conn-edit-id" value=""> <div class="set-field"> <label data-i18n="I18N_CONN_GROUP">Gruppe</label> Loading Loading @@ -297,7 +303,7 @@ <span id="conn-status" class="set-status"></span> </div> <div id="conn-placeholder"> <p class="hint" data-i18n="I18N_SELECT_CONNECTION">Verbindung auswählen oder neue hinzufügen.</p> <p class="hint" data-i18n="I18N_SELECT_PROJECT">Projekt auswählen oder neues hinzufügen.</p> </div> </div> </div> Loading Loading @@ -329,7 +335,7 @@ </div> </div> <div class="dialog-actions"> <button id="btn-publish-settings" data-i18n="Settings" style="margin-right:auto">⚙ <span data-i18n="I18N_CONNECTIONS">Verbindungen</span></button> <button id="btn-publish-settings" data-i18n="Settings" style="margin-right:auto">⚙ <span data-i18n="I18N_PROJECTS">Projekte</span></button> <button id="btn-publish-close" data-i18n="Close">Schließen</button> </div> </dialog> Loading Loading @@ -404,7 +410,7 @@ <ul class="set-nav"> <li class="set-nav-item active" data-section="language" data-i18n="Language">Sprache</li> <li class="set-nav-item" data-section="ai" data-i18n="I18N_AI">KI</li> <li class="set-nav-item" data-section="connection" data-i18n="I18N_CONNECTION">Verbindung</li> <li class="set-nav-item" data-section="connection" data-i18n="I18N_PROJECT">Projekt</li> <li class="set-nav-item" data-section="mcp" data-i18n="I18N_MCP">MCP</li> </ul> </div> Loading Loading @@ -452,10 +458,10 @@ <!-- Connection section --> <div id="settings-connection" class="set-section" style="display:none"> <h4 data-i18n="I18N_CONNECTIONS">Verbindungen</h4> <p class="hint" data-i18n="I18N_CONN_MANAGER_HINT">Verbindungen werden im Verbindungsmanager verwaltet.</p> <h4 data-i18n="I18N_PROJECTS">Projekte</h4> <p class="hint" data-i18n="I18N_PROJECT_MANAGER_HINT">Projekte werden im Projekt-Manager verwaltet.</p> <div class="set-field"> <button id="btn-settings-open-connmgr" class="set-save-btn" data-i18n="I18N_OPEN_CONN_MANAGER">Verbindungsmanager öffnen</button> <button id="btn-settings-open-connmgr" class="set-save-btn" data-i18n="I18N_OPEN_PROJECT_MANAGER">Projekt-Manager öffnen</button> </div> </div> Loading
editor/html/js/api.js +24 −8 Changes for editor/html/js/api.js: 24 added lines, 8 removed lines. Original line number Diff line number Diff line Loading @@ -95,6 +95,21 @@ var EditorApi = (function() { return { promise: promise, abort: function() { xhr.abort(); } }; } // Endpoints scoped to a connection/project (publish, media, survey) require a live // server-side session (_connSessions in webedit_api.cpp) that can be evicted (auth // expired, server restart, ...). Those handlers reply 403 "Not connected...". Rather // than surfacing that to the user, reconnect once and retry transparently. function requestWithReconnect(connId, method, url, data) { return request(method, url, data).catch(function(err) { if (err && typeof err.error === 'string' && err.error.indexOf('Not connected') === 0) { return request('POST', '/api/connection/connect/' + connId, {}).then(function() { return request(method, url, data); }); } return Promise.reject(err); }); } return { login: function(username, password) { return request('POST', '/api/login', { Loading Loading @@ -379,7 +394,7 @@ var EditorApi = (function() { }, publishToConnection: function(id, target, params) { return request('POST', '/api/connection/publish/' + id, { return requestWithReconnect(id, 'POST', '/api/connection/publish/' + id, { target: target, params: params || {} }); Loading @@ -387,19 +402,19 @@ var EditorApi = (function() { // Media browse (via blog connection) mediaListAlbums: function(connId) { return request('POST', '/api/connection/media/' + connId, { return requestWithReconnect(connId, 'POST', '/api/connection/media/' + connId, { command: 'media_list_albums' }); }, mediaListStores: function(connId) { return request('POST', '/api/connection/media/' + connId, { return requestWithReconnect(connId, 'POST', '/api/connection/media/' + connId, { command: 'media_list_stores' }); }, mediaCreateAlbum: function(connId, storeName, albumName, isPublic) { return request('POST', '/api/connection/media/' + connId, { return requestWithReconnect(connId, 'POST', '/api/connection/media/' + connId, { command: 'media_create_album', store_name: storeName, album_name: albumName, Loading @@ -408,7 +423,7 @@ var EditorApi = (function() { }, mediaUpload: function(connId, albumId, filename, dataBase64) { return request('POST', '/api/connection/media/' + connId, { return requestWithReconnect(connId, 'POST', '/api/connection/media/' + connId, { command: 'media_upload', album_id: albumId, filename: filename, Loading @@ -417,7 +432,7 @@ var EditorApi = (function() { }, mediaListMedia: function(connId, albumId) { return request('POST', '/api/connection/media/' + connId, { return requestWithReconnect(connId, 'POST', '/api/connection/media/' + connId, { command: 'media_list_media', album_id: albumId }); Loading @@ -430,7 +445,7 @@ var EditorApi = (function() { }; if (width > 0) params.width = width; if (height > 0) params.height = height; return request('POST', '/api/connection/media/' + connId, params); return requestWithReconnect(connId, 'POST', '/api/connection/media/' + connId, params); }, // HTML import/export Loading Loading @@ -471,7 +486,8 @@ var EditorApi = (function() { }, publishSurvey: function(surveyId, connId) { return request('POST', '/api/survey/publish', { survey_id: surveyId, conn_id: connId }); return requestWithReconnect(connId, 'POST', '/api/survey/publish', { survey_id: surveyId, conn_id: connId }); }, // Plugin settings (served by the backend settings router via AJAX) Loading
editor/html/js/editor.js +152 −5 Changes for editor/html/js/editor.js: 152 added lines, 5 removed lines. Original line number Diff line number Diff line Loading @@ -10,6 +10,10 @@ var currentDocGroup = ''; var currentDocSeo = {}; var saveDialogMode = 'save'; var projectsCache = []; var currentProjectId = ''; var currentProjectName = ''; var CURRENT_PROJECT_STORAGE_KEY = 'blogi-current-project'; var wizardTemplates = []; var wizardCsvRows = []; var wizardCsvColumns = []; Loading @@ -33,6 +37,7 @@ try { SurveyManager.init(); } catch(e) { console.error('SurveyManager.init:', e); } try { Preview.bindToggle(); } catch(e) { console.error('Preview.bindToggle:', e); } try { Canvas.bindMessageListener(); } catch(e) { console.error('Canvas.bindMessageListener:', e); } try { initProjectSelector(); } catch(e) { console.error('initProjectSelector:', e); } Preview.clear(); // Selection can be driven either by clicking directly on an Loading Loading @@ -225,7 +230,11 @@ document.getElementById('btn-open').addEventListener('click', function() { EditorApi.listDocuments().then(function(resp) { renderDocList(resp.documents || []); var docs = resp.documents || []; if (currentProjectName) { docs = docs.filter(function(d) { return (d.group || '') === currentProjectName; }); } renderDocList(docs); document.getElementById('open-dialog').showModal(); }); }); Loading Loading @@ -365,10 +374,37 @@ I18n.t('I18N_SAVE_AS_TPL_TITLE', 'Dokument speichern unter') : I18n.t('I18N_SAVE_TPL_TITLE', 'Dokument speichern'); document.getElementById('save-name').value = name || ''; document.getElementById('save-group').value = group || ''; populateSaveGroupSelect(group || currentProjectName || ''); document.getElementById('save-dialog').showModal(); } // Populates the "Speichern unter" project select from projectsCache. `selectedGroup` // is preselected if it matches a known project name; otherwise (legacy free-text // groups from before projects existed) it's appended as an extra option so existing // documents don't silently lose their group on the next save. function populateSaveGroupSelect(selectedGroup) { var select = document.getElementById('save-group'); select.innerHTML = ''; var blankOpt = document.createElement('option'); blankOpt.value = ''; blankOpt.textContent = I18n.t('I18N_NO_PROJECT', 'Kein Projekt'); select.appendChild(blankOpt); appendGroupedProjectOptions(select, function(p) { return p.name; }); var hasSelected = !selectedGroup; var options = select.querySelectorAll('option'); for (var i = 0; i < options.length; i++) { if (options[i].value === selectedGroup) { hasSelected = true; break; } } if (selectedGroup && !hasSelected) { var extra = document.createElement('option'); extra.value = selectedGroup; extra.textContent = selectedGroup; select.appendChild(extra); } select.value = selectedGroup || ''; } // Fills the document-level SEO & Social dialog from currentDocSeo (see // DocumentState::seo / <WebEditor><SeoMeta> in webedit_api.cpp) — this // metadata travels with the saved template's XML, unlike the per-publish Loading Loading @@ -1665,6 +1701,111 @@ } } // --- Project selector (toolbar) --- // A "project" is the same underlying entity as a saved connection (name/group/url/ // ignore_ssl — see EditorApi.listConnections/connectToConnection and the project // manager dialog below). The toolbar select exposes it under project terminology: // picking one auto-connects and becomes the default group when saving documents. function initProjectSelector() { document.getElementById('project-select').addEventListener('change', function() { onProjectSelected(this.value); }); loadProjectSelectOptions(function() { var saved = ''; try { saved = localStorage.getItem(CURRENT_PROJECT_STORAGE_KEY) || ''; } catch(e) {} if (saved && projectsCache.some(function(p) { return p.id === saved; })) { document.getElementById('project-select').value = saved; onProjectSelected(saved); } }); } // Fetches the project list and (re)renders the toolbar select; shared with the // project manager dialog (loadConnectionList) so both stay in sync after add/edit/delete. function loadProjectSelectOptions(callback) { EditorApi.listConnections().then(function(resp) { projectsCache = resp.connections || []; renderProjectToolbarSelect(); if (callback) callback(); }); } function renderProjectToolbarSelect() { var select = document.getElementById('project-select'); select.innerHTML = '<option value="">' + I18n.t('I18N_ALL_PROJECTS', 'Alle Projekte') + '</option>'; appendGroupedProjectOptions(select, function(p) { return p.id; }); select.value = currentProjectId || ''; } // Builds <optgroup>-grouped <option>s (grouped like the project manager's sidebar // list) into `select` from projectsCache; valueFn picks the option value per project. function appendGroupedProjectOptions(select, valueFn) { var groups = {}; for (var i = 0; i < projectsCache.length; i++) { var g = projectsCache[i].group || ''; if (!groups[g]) groups[g] = []; groups[g].push(projectsCache[i]); } var groupNames = Object.keys(groups).sort(); for (var gi = 0; gi < groupNames.length; gi++) { var gName = groupNames[gi]; var container = select; if (gName) { container = document.createElement('optgroup'); container.label = gName; select.appendChild(container); } var projs = groups[gName]; for (var pi = 0; pi < projs.length; pi++) { var opt = document.createElement('option'); opt.value = valueFn(projs[pi]); opt.textContent = projs[pi].name; container.appendChild(opt); } } } function onProjectSelected(id) { var statusEl = document.getElementById('project-status'); if (!id) { currentProjectId = ''; currentProjectName = ''; try { localStorage.removeItem(CURRENT_PROJECT_STORAGE_KEY); } catch(e) {} statusEl.textContent = ''; statusEl.title = ''; return; } var proj = null; for (var i = 0; i < projectsCache.length; i++) { if (projectsCache[i].id === id) { proj = projectsCache[i]; break; } } currentProjectId = id; currentProjectName = proj ? proj.name : ''; try { localStorage.setItem(CURRENT_PROJECT_STORAGE_KEY, id); } catch(e) {} connectCurrentProject(); } // Auto-connect (and, via the "Not connected" retry in EditorApi, auto-reconnect on // the next connection-scoped call) whenever the current project changes or is // restored from localStorage on load. function connectCurrentProject() { if (!currentProjectId) return; var statusEl = document.getElementById('project-status'); statusEl.textContent = I18n.t('I18N_CONNECTING', 'Verbinde...'); statusEl.title = ''; EditorApi.connectToConnection(currentProjectId).then(function(resp) { connectedConnections[currentProjectId] = true; statusEl.textContent = '✓'; statusEl.title = I18n.t('I18N_CONNECTION_OK', 'Verbunden'); showPreviewPublishTargets(resp.targets || []); }).catch(function(err) { statusEl.textContent = '✗'; statusEl.title = I18n.t('I18N_CONNECTION_FAILED', 'Verbindung fehlgeschlagen') + ': ' + ((err && err.error) || ''); }); } // --- Connection Manager --- // Track which connections are currently "connected" (have authid on server) Loading Loading @@ -1738,6 +1879,12 @@ if (!confirm(I18n.t('I18N_REMOVE_CONFIRM', 'Wirklich l\u00f6schen') + '?')) return; EditorApi.deleteConnection(id).then(function() { delete connectedConnections[id]; if (id === currentProjectId) { currentProjectId = ''; currentProjectName = ''; try { localStorage.removeItem(CURRENT_PROJECT_STORAGE_KEY); } catch(e) {} document.getElementById('project-status').textContent = ''; } document.getElementById('conn-form').style.display = 'none'; document.getElementById('conn-placeholder').style.display = 'block'; loadConnectionList(); Loading Loading @@ -1958,8 +2105,8 @@ } function loadConnectionList() { EditorApi.listConnections().then(function(resp) { renderConnectionList(resp.connections || []); loadProjectSelectOptions(function() { renderConnectionList(projectsCache); }); } Loading Loading @@ -2023,7 +2170,7 @@ if (connections.length === 0) { list.innerHTML = '<p class="hint" style="padding:0.5em">' + I18n.t('I18N_NO_CONNECTIONS', 'Keine Verbindungen') + '</p>'; I18n.t('I18N_NO_PROJECTS', 'Keine Projekte') + '</p>'; } } Loading
src/translations.h +18 −0 Changes for src/translations.h: 18 added lines, 0 removed lines. Original line number Diff line number Diff line Loading @@ -392,6 +392,15 @@ namespace blogi { {"I18N_NO_CONNECTIONS", "Keine Verbindungen"}, {"I18N_CONN_MANAGER_HINT", "Verbindungen werden im Verbindungsmanager verwaltet."}, {"I18N_OPEN_CONN_MANAGER", "Verbindungsmanager öffnen"}, // Projects (editor toolbar/dialog wording for the same connection entity) {"I18N_PROJECT", "Projekt"}, {"I18N_PROJECTS", "Projekte"}, {"I18N_SELECT_PROJECT", "Projekt auswählen oder neues hinzufügen."}, {"I18N_NO_PROJECTS", "Keine Projekte"}, {"I18N_NO_PROJECT", "Kein Projekt"}, {"I18N_ALL_PROJECTS", "Alle Projekte"}, {"I18N_PROJECT_MANAGER_HINT","Projekte werden im Projekt-Manager verwaltet."}, {"I18N_OPEN_PROJECT_MANAGER","Projekt-Manager öffnen"}, {"I18N_ADD", "Hinzufügen"}, {"I18N_DELETE", "Löschen"}, {"I18N_FIELDS_REQUIRED", "Name und URL sind erforderlich"}, Loading Loading @@ -855,6 +864,15 @@ namespace blogi { {"I18N_NO_CONNECTIONS", "No connections"}, {"I18N_CONN_MANAGER_HINT", "Connections are managed in the Connection Manager."}, {"I18N_OPEN_CONN_MANAGER", "Open Connection Manager"}, // Projects (editor toolbar/dialog wording for the same connection entity) {"I18N_PROJECT", "Project"}, {"I18N_PROJECTS", "Projects"}, {"I18N_SELECT_PROJECT", "Select a project or add a new one."}, {"I18N_NO_PROJECTS", "No projects"}, {"I18N_NO_PROJECT", "No project"}, {"I18N_ALL_PROJECTS", "All projects"}, {"I18N_PROJECT_MANAGER_HINT","Projects are managed in the Project Manager."}, {"I18N_OPEN_PROJECT_MANAGER","Open Project Manager"}, {"I18N_ADD", "Add"}, {"I18N_DELETE", "Delete"}, {"I18N_FIELDS_REQUIRED", "Name and URL are required"}, Loading