Loading editor/html/js/editor.js +22 −6 Changes for editor/html/js/editor.js: 22 added lines, 6 removed lines. Original line number Diff line number Diff line Loading @@ -9,6 +9,7 @@ var currentDocId = ''; var currentDocGroup = ''; var currentDocSeo = {}; var currentDocPublishParams = {}; // target name -> last-used publish params for this template var saveDialogMode = 'save'; var projectsCache = []; var currentProjectId = ''; Loading Loading @@ -185,6 +186,7 @@ document.getElementById('doc-name').textContent = resp.name || 'Untitled'; currentDocId = resp.id || ''; currentDocSeo = resp.seo || {}; currentDocPublishParams = resp.publish_params || {}; Canvas.setTreeData(resp.tree || []); DocumentTree.render(resp.tree || []); Preview.refresh(); Loading Loading @@ -1975,7 +1977,10 @@ // the main publish dialog, and the SEO & Social sub-dialog). "image" opens // the existing MediaBrowser (same pattern as properties.js/survey-manager.js) // instead of taking free text. function createPublishFieldInput(field, styleCss) { function createPublishFieldInput(field, styleCss, savedParams) { var savedValue = savedParams ? savedParams[field.key] : undefined; var hasSaved = savedValue !== undefined && savedValue !== null; if (field.type === 'select' && field.options) { var sel = document.createElement('select'); sel.setAttribute('data-publish-key', field.key); Loading @@ -1986,6 +1991,7 @@ optEl.textContent = field.options[o].label; sel.appendChild(optEl); } if (hasSaved) sel.value = savedValue; return sel; } if (field.type === 'textarea') { Loading @@ -1993,6 +1999,7 @@ ta.setAttribute('data-publish-key', field.key); if (styleCss) ta.style.cssText = styleCss; if (field.placeholder) ta.placeholder = field.placeholder; if (hasSaved) ta.value = savedValue; return ta; } if (field.type === 'image') { Loading @@ -2004,6 +2011,7 @@ imgInp.setAttribute('data-publish-key', field.key); imgInp.style.cssText = (styleCss || '') + 'flex:1;'; if (field.placeholder) imgInp.placeholder = field.placeholder; if (hasSaved) imgInp.value = savedValue; var chooseBtn = document.createElement('button'); chooseBtn.type = 'button'; chooseBtn.textContent = I18n.t('I18N_CHOOSE_IMAGE', 'Wählen…'); Loading @@ -2023,6 +2031,10 @@ if (styleCss) inp.style.cssText = styleCss; if (field.placeholder) inp.placeholder = field.placeholder; if (field.required) inp.required = true; if (hasSaved) { if (inp.type === 'checkbox') inp.checked = !!savedValue; else inp.value = savedValue; } return inp; } Loading @@ -2048,6 +2060,7 @@ container.style.gap = '0.4em'; container.style.alignItems = 'center'; var fieldStyle = 'padding:0.15em 0.3em;border-radius:3px;border:1px solid var(--border);background:var(--bg-secondary);color:var(--text-primary);font-size:0.75rem;width:8em;'; var savedParams = currentDocPublishParams[targetName] || {}; for (var f = 0; f < fields.length; f++) { var field = fields[f]; var label = document.createElement('label'); Loading @@ -2055,7 +2068,7 @@ label.style.fontSize = '0.75rem'; label.style.color = 'var(--text-secondary)'; container.appendChild(label); container.appendChild(createPublishFieldInput(field, fieldStyle)); container.appendChild(createPublishFieldInput(field, fieldStyle, savedParams)); } } Loading Loading @@ -2101,6 +2114,7 @@ } return; } currentDocPublishParams[target] = params; publishBtn.innerHTML = '✅ OK'; setTimeout(function() { publishBtn.innerHTML = origText; Loading Loading @@ -2273,13 +2287,13 @@ renderPublishDialogFields(targets[0].name); } function appendFormFieldRow(container, field) { function appendFormFieldRow(container, field, savedParams) { var div = document.createElement('div'); div.className = 'set-field'; var label = document.createElement('label'); label.textContent = field.label || field.key; div.appendChild(label); div.appendChild(createPublishFieldInput(field)); div.appendChild(createPublishFieldInput(field, undefined, savedParams)); container.appendChild(div); } Loading @@ -2302,13 +2316,14 @@ } var fields = target.fields; var hasSeoFields = false; var savedParams = currentDocPublishParams[targetName] || {}; for (var f = 0; f < fields.length; f++) { var field = fields[f]; if (field.group === 'seo') { hasSeoFields = true; appendFormFieldRow(seoContainer, field); appendFormFieldRow(seoContainer, field, savedParams); } else { appendFormFieldRow(container, field); appendFormFieldRow(container, field, savedParams); } } seoBtn.style.display = hasSeoFields ? '' : 'none'; Loading Loading @@ -2377,6 +2392,7 @@ statusEl.className = 'set-status'; EditorApi.publishToConnection(connId, target, params).then(function() { currentDocPublishParams[target] = params; statusEl.textContent = I18n.t('I18N_PUBLISH_SUCCESS', 'Erfolgreich ver\u00f6ffentlicht'); statusEl.className = 'set-status success'; }).catch(function(err) { Loading editor/src/webedit_api.cpp +58 −1 Changes for editor/src/webedit_api.cpp: 58 added lines, 1 removed line. Original line number Diff line number Diff line Loading @@ -98,6 +98,17 @@ json_object* seoMetaToJson(const blogi::SeoMeta &seo) { return obj; } // { "<target name>": {...last-used publish params...}, ... } for the editor to pre-fill // the publish dialog's dynamic fields for whichever target the user picks. json_object* publishParamsMapToJson(const std::map<std::string, std::string> ¶ms) { json_object *obj = json_object_new_object(); for (auto &kv : params) { json_object *parsed = json_tokener_parse(kv.second.c_str()); json_object_object_add(obj, kv.first.c_str(), parsed ? parsed : json_object_new_object()); } return obj; } std::string trimCopy(const std::string &value) { size_t start = 0; while (start < value.size() && std::isspace(static_cast<unsigned char>(value[start]))) { Loading Loading @@ -494,6 +505,7 @@ void webedit::Api::loadDocumentTreeFromXml(DocumentState &doc, const std::string doc.root = nullptr; doc.ownedElements.clear(); doc.seo = blogi::SeoMeta(); doc.publishParams.clear(); tinyxml2::XMLDocument xmlDoc; if (xmlDoc.Parse(xml.c_str()) != tinyxml2::XML_SUCCESS) Loading @@ -520,9 +532,19 @@ void webedit::Api::loadDocumentTreeFromXml(DocumentState &doc, const std::string } } if (auto *paramsEl = rootEl->FirstChildElement("PublishParams")) { for (auto *targetEl = paramsEl->FirstChildElement("Target"); targetEl; targetEl = targetEl->NextSiblingElement("Target")) { const char *name = targetEl->Attribute("name"); const char *text = targetEl->GetText(); if (name && text) doc.publishParams[name] = text; } } blogi::webedit::EditPlugin *lastTop = nullptr; for (auto *child = rootEl->FirstChildElement(); child; child = child->NextSiblingElement()) { if (strcmp(child->Name(), "property") == 0 || strcmp(child->Name(), "SeoMeta") == 0) if (strcmp(child->Name(), "property") == 0 || strcmp(child->Name(), "SeoMeta") == 0 || strcmp(child->Name(), "PublishParams") == 0) continue; const char *typeName = child->Name(); Loading Loading @@ -945,6 +967,7 @@ void webedit::Api::handleNewDocument(libhttppp::HttpRequest &curreq, doc.currentDocName = "Untitled"; doc.currentDocGroup.clear(); doc.seo = blogi::SeoMeta(); doc.publishParams.clear(); persistDocumentStateToSession(sessionid, doc); json_object *resp = json_object_new_object(); Loading @@ -965,6 +988,7 @@ void webedit::Api::handleGetDocument(libhttppp::HttpRequest &curreq, json_object_object_add(resp, "name", json_object_new_string(doc.currentDocName.c_str())); json_object_object_add(resp, "id", json_object_new_string(doc.currentDocId.c_str())); json_object_object_add(resp, "seo", seoMetaToJson(doc.seo)); json_object_object_add(resp, "publish_params", publishParamsMapToJson(doc.publishParams)); json_object *tree = json_object_new_array(); auto *el = doc.root; Loading Loading @@ -4024,6 +4048,20 @@ void webedit::Api::handlePublishToConnection(libhttppp::HttpRequest &curreq, json_object_put(req); if (result.ok) { // publishToConnectionCore() just updated doc.publishParams in memory on an // actual successful publish — write it back to the template itself (session // cookie for this session, DB row if the template has already been saved) so // it's there to pre-fill the next time this template is opened/published. if (result.status == "success" || result.status == "ok") { persistDocumentStateToSession(sessionid, doc); if (!doc.currentDocId.empty()) { std::string author; _session.getData(sessionid, "username", author); _db.updateDocument(domain, doc.currentDocId, doc.currentDocName, exportTreeXml(doc), author, doc.currentDocGroup); } } json_object *resp = json_object_new_object(); json_object_object_add(resp, "status", json_object_new_string(result.status.c_str())); json_object_object_add(resp, "message", json_object_new_string(result.message.c_str())); Loading Loading @@ -4114,6 +4152,14 @@ webedit::Api::PublishCoreResult webedit::Api::publishToConnectionCore( if (pubStatus == "success" || pubStatus == "ok" || pubStatus == "duplicate") { result.ok = true; // Remember these params on the template itself (not just this publish) so // the next publish to the same target starts pre-filled. Only once the blog // actually accepted the content — "duplicate" is still an unresolved // overwrite prompt at this point, not a completed publish. if (paramsObj && (pubStatus == "success" || pubStatus == "ok")) { doc.publishParams[target] = json_object_to_json_string_ext(paramsObj, JSON_C_TO_STRING_NOSLASHESCAPE); } } else { // Auth might have expired if (pubStatus == "error") { Loading Loading @@ -4860,6 +4906,17 @@ std::string webedit::Api::exportTreeXml(DocumentState &doc) { root->InsertEndChild(seoEl); } if (!doc.publishParams.empty()) { auto *paramsEl = xmlDoc.NewElement("PublishParams"); for (auto &kv : doc.publishParams) { auto *targetEl = xmlDoc.NewElement("Target"); targetEl->SetAttribute("name", kv.first.c_str()); targetEl->SetText(kv.second.c_str()); paramsEl->InsertEndChild(targetEl); } root->InsertEndChild(paramsEl); } auto *el = doc.root; while (el) { auto *xmlEl = el->toXmlObject(&xmlDoc); Loading editor/src/webedit_api.h +4 −0 Changes for editor/src/webedit_api.h: 4 added lines, 0 removed lines. Original line number Diff line number Diff line Loading @@ -118,6 +118,10 @@ namespace webedit { std::string currentDocName; std::string currentDocGroup; blogi::SeoMeta seo; // round-tripped via the <WebEditor> root's seo_* attributes // Last-used publish params per target name (raw JSON text), round-tripped // via <WebEditor><PublishParams><Target name="...">...</Target></PublishParams> // so the publish dialog can pre-fill them next time this template is opened. std::map<std::string, std::string> publishParams; std::vector<std::unique_ptr<blogi::webedit::EditPlugin, blogi::webedit::EditPluginDeleter>> ownedElements; std::mutex mtx; Loading Loading
editor/html/js/editor.js +22 −6 Changes for editor/html/js/editor.js: 22 added lines, 6 removed lines. Original line number Diff line number Diff line Loading @@ -9,6 +9,7 @@ var currentDocId = ''; var currentDocGroup = ''; var currentDocSeo = {}; var currentDocPublishParams = {}; // target name -> last-used publish params for this template var saveDialogMode = 'save'; var projectsCache = []; var currentProjectId = ''; Loading Loading @@ -185,6 +186,7 @@ document.getElementById('doc-name').textContent = resp.name || 'Untitled'; currentDocId = resp.id || ''; currentDocSeo = resp.seo || {}; currentDocPublishParams = resp.publish_params || {}; Canvas.setTreeData(resp.tree || []); DocumentTree.render(resp.tree || []); Preview.refresh(); Loading Loading @@ -1975,7 +1977,10 @@ // the main publish dialog, and the SEO & Social sub-dialog). "image" opens // the existing MediaBrowser (same pattern as properties.js/survey-manager.js) // instead of taking free text. function createPublishFieldInput(field, styleCss) { function createPublishFieldInput(field, styleCss, savedParams) { var savedValue = savedParams ? savedParams[field.key] : undefined; var hasSaved = savedValue !== undefined && savedValue !== null; if (field.type === 'select' && field.options) { var sel = document.createElement('select'); sel.setAttribute('data-publish-key', field.key); Loading @@ -1986,6 +1991,7 @@ optEl.textContent = field.options[o].label; sel.appendChild(optEl); } if (hasSaved) sel.value = savedValue; return sel; } if (field.type === 'textarea') { Loading @@ -1993,6 +1999,7 @@ ta.setAttribute('data-publish-key', field.key); if (styleCss) ta.style.cssText = styleCss; if (field.placeholder) ta.placeholder = field.placeholder; if (hasSaved) ta.value = savedValue; return ta; } if (field.type === 'image') { Loading @@ -2004,6 +2011,7 @@ imgInp.setAttribute('data-publish-key', field.key); imgInp.style.cssText = (styleCss || '') + 'flex:1;'; if (field.placeholder) imgInp.placeholder = field.placeholder; if (hasSaved) imgInp.value = savedValue; var chooseBtn = document.createElement('button'); chooseBtn.type = 'button'; chooseBtn.textContent = I18n.t('I18N_CHOOSE_IMAGE', 'Wählen…'); Loading @@ -2023,6 +2031,10 @@ if (styleCss) inp.style.cssText = styleCss; if (field.placeholder) inp.placeholder = field.placeholder; if (field.required) inp.required = true; if (hasSaved) { if (inp.type === 'checkbox') inp.checked = !!savedValue; else inp.value = savedValue; } return inp; } Loading @@ -2048,6 +2060,7 @@ container.style.gap = '0.4em'; container.style.alignItems = 'center'; var fieldStyle = 'padding:0.15em 0.3em;border-radius:3px;border:1px solid var(--border);background:var(--bg-secondary);color:var(--text-primary);font-size:0.75rem;width:8em;'; var savedParams = currentDocPublishParams[targetName] || {}; for (var f = 0; f < fields.length; f++) { var field = fields[f]; var label = document.createElement('label'); Loading @@ -2055,7 +2068,7 @@ label.style.fontSize = '0.75rem'; label.style.color = 'var(--text-secondary)'; container.appendChild(label); container.appendChild(createPublishFieldInput(field, fieldStyle)); container.appendChild(createPublishFieldInput(field, fieldStyle, savedParams)); } } Loading Loading @@ -2101,6 +2114,7 @@ } return; } currentDocPublishParams[target] = params; publishBtn.innerHTML = '✅ OK'; setTimeout(function() { publishBtn.innerHTML = origText; Loading Loading @@ -2273,13 +2287,13 @@ renderPublishDialogFields(targets[0].name); } function appendFormFieldRow(container, field) { function appendFormFieldRow(container, field, savedParams) { var div = document.createElement('div'); div.className = 'set-field'; var label = document.createElement('label'); label.textContent = field.label || field.key; div.appendChild(label); div.appendChild(createPublishFieldInput(field)); div.appendChild(createPublishFieldInput(field, undefined, savedParams)); container.appendChild(div); } Loading @@ -2302,13 +2316,14 @@ } var fields = target.fields; var hasSeoFields = false; var savedParams = currentDocPublishParams[targetName] || {}; for (var f = 0; f < fields.length; f++) { var field = fields[f]; if (field.group === 'seo') { hasSeoFields = true; appendFormFieldRow(seoContainer, field); appendFormFieldRow(seoContainer, field, savedParams); } else { appendFormFieldRow(container, field); appendFormFieldRow(container, field, savedParams); } } seoBtn.style.display = hasSeoFields ? '' : 'none'; Loading Loading @@ -2377,6 +2392,7 @@ statusEl.className = 'set-status'; EditorApi.publishToConnection(connId, target, params).then(function() { currentDocPublishParams[target] = params; statusEl.textContent = I18n.t('I18N_PUBLISH_SUCCESS', 'Erfolgreich ver\u00f6ffentlicht'); statusEl.className = 'set-status success'; }).catch(function(err) { Loading
editor/src/webedit_api.cpp +58 −1 Changes for editor/src/webedit_api.cpp: 58 added lines, 1 removed line. Original line number Diff line number Diff line Loading @@ -98,6 +98,17 @@ json_object* seoMetaToJson(const blogi::SeoMeta &seo) { return obj; } // { "<target name>": {...last-used publish params...}, ... } for the editor to pre-fill // the publish dialog's dynamic fields for whichever target the user picks. json_object* publishParamsMapToJson(const std::map<std::string, std::string> ¶ms) { json_object *obj = json_object_new_object(); for (auto &kv : params) { json_object *parsed = json_tokener_parse(kv.second.c_str()); json_object_object_add(obj, kv.first.c_str(), parsed ? parsed : json_object_new_object()); } return obj; } std::string trimCopy(const std::string &value) { size_t start = 0; while (start < value.size() && std::isspace(static_cast<unsigned char>(value[start]))) { Loading Loading @@ -494,6 +505,7 @@ void webedit::Api::loadDocumentTreeFromXml(DocumentState &doc, const std::string doc.root = nullptr; doc.ownedElements.clear(); doc.seo = blogi::SeoMeta(); doc.publishParams.clear(); tinyxml2::XMLDocument xmlDoc; if (xmlDoc.Parse(xml.c_str()) != tinyxml2::XML_SUCCESS) Loading @@ -520,9 +532,19 @@ void webedit::Api::loadDocumentTreeFromXml(DocumentState &doc, const std::string } } if (auto *paramsEl = rootEl->FirstChildElement("PublishParams")) { for (auto *targetEl = paramsEl->FirstChildElement("Target"); targetEl; targetEl = targetEl->NextSiblingElement("Target")) { const char *name = targetEl->Attribute("name"); const char *text = targetEl->GetText(); if (name && text) doc.publishParams[name] = text; } } blogi::webedit::EditPlugin *lastTop = nullptr; for (auto *child = rootEl->FirstChildElement(); child; child = child->NextSiblingElement()) { if (strcmp(child->Name(), "property") == 0 || strcmp(child->Name(), "SeoMeta") == 0) if (strcmp(child->Name(), "property") == 0 || strcmp(child->Name(), "SeoMeta") == 0 || strcmp(child->Name(), "PublishParams") == 0) continue; const char *typeName = child->Name(); Loading Loading @@ -945,6 +967,7 @@ void webedit::Api::handleNewDocument(libhttppp::HttpRequest &curreq, doc.currentDocName = "Untitled"; doc.currentDocGroup.clear(); doc.seo = blogi::SeoMeta(); doc.publishParams.clear(); persistDocumentStateToSession(sessionid, doc); json_object *resp = json_object_new_object(); Loading @@ -965,6 +988,7 @@ void webedit::Api::handleGetDocument(libhttppp::HttpRequest &curreq, json_object_object_add(resp, "name", json_object_new_string(doc.currentDocName.c_str())); json_object_object_add(resp, "id", json_object_new_string(doc.currentDocId.c_str())); json_object_object_add(resp, "seo", seoMetaToJson(doc.seo)); json_object_object_add(resp, "publish_params", publishParamsMapToJson(doc.publishParams)); json_object *tree = json_object_new_array(); auto *el = doc.root; Loading Loading @@ -4024,6 +4048,20 @@ void webedit::Api::handlePublishToConnection(libhttppp::HttpRequest &curreq, json_object_put(req); if (result.ok) { // publishToConnectionCore() just updated doc.publishParams in memory on an // actual successful publish — write it back to the template itself (session // cookie for this session, DB row if the template has already been saved) so // it's there to pre-fill the next time this template is opened/published. if (result.status == "success" || result.status == "ok") { persistDocumentStateToSession(sessionid, doc); if (!doc.currentDocId.empty()) { std::string author; _session.getData(sessionid, "username", author); _db.updateDocument(domain, doc.currentDocId, doc.currentDocName, exportTreeXml(doc), author, doc.currentDocGroup); } } json_object *resp = json_object_new_object(); json_object_object_add(resp, "status", json_object_new_string(result.status.c_str())); json_object_object_add(resp, "message", json_object_new_string(result.message.c_str())); Loading Loading @@ -4114,6 +4152,14 @@ webedit::Api::PublishCoreResult webedit::Api::publishToConnectionCore( if (pubStatus == "success" || pubStatus == "ok" || pubStatus == "duplicate") { result.ok = true; // Remember these params on the template itself (not just this publish) so // the next publish to the same target starts pre-filled. Only once the blog // actually accepted the content — "duplicate" is still an unresolved // overwrite prompt at this point, not a completed publish. if (paramsObj && (pubStatus == "success" || pubStatus == "ok")) { doc.publishParams[target] = json_object_to_json_string_ext(paramsObj, JSON_C_TO_STRING_NOSLASHESCAPE); } } else { // Auth might have expired if (pubStatus == "error") { Loading Loading @@ -4860,6 +4906,17 @@ std::string webedit::Api::exportTreeXml(DocumentState &doc) { root->InsertEndChild(seoEl); } if (!doc.publishParams.empty()) { auto *paramsEl = xmlDoc.NewElement("PublishParams"); for (auto &kv : doc.publishParams) { auto *targetEl = xmlDoc.NewElement("Target"); targetEl->SetAttribute("name", kv.first.c_str()); targetEl->SetText(kv.second.c_str()); paramsEl->InsertEndChild(targetEl); } root->InsertEndChild(paramsEl); } auto *el = doc.root; while (el) { auto *xmlEl = el->toXmlObject(&xmlDoc); Loading
editor/src/webedit_api.h +4 −0 Changes for editor/src/webedit_api.h: 4 added lines, 0 removed lines. Original line number Diff line number Diff line Loading @@ -118,6 +118,10 @@ namespace webedit { std::string currentDocName; std::string currentDocGroup; blogi::SeoMeta seo; // round-tripped via the <WebEditor> root's seo_* attributes // Last-used publish params per target name (raw JSON text), round-tripped // via <WebEditor><PublishParams><Target name="...">...</Target></PublishParams> // so the publish dialog can pre-fill them next time this template is opened. std::map<std::string, std::string> publishParams; std::vector<std::unique_ptr<blogi::webedit::EditPlugin, blogi::webedit::EditPluginDeleter>> ownedElements; std::mutex mtx; Loading