inclide craft

This commit is contained in:
2026-01-19 00:10:26 +01:00
parent 8a23398853
commit 05fd31065d
12 changed files with 624 additions and 55 deletions

View File

@@ -68,6 +68,11 @@ require dirname(__DIR__) . '/../structure/layout_start.php';
<button type="button" class="btn" data-rotate="external">Neu erstellen</button>
</div>
</div>
<label class="block text-sm text-slate-600">Standard-Editor</label>
<select name="editor_default" class="input">
<option value="grapesjs">GrapesJS</option>
<option value="craftjs">Craft.js</option>
</select>
<div class="flex justify-between gap-2 flex-wrap pt-2">
<div class="flex gap-2" data-role="admin">
<button type="button" class="btn" data-download="bridge">Bridge-Datei</button>

View File

@@ -66,6 +66,14 @@ if ($debugRedirect) {
<?php endif; ?>
</script>
<script src="https://cdn.tailwindcss.com"></script>
<script type="importmap">
{
"imports": {
"react": "/vendor/react@18.2.0/es2022/react.mjs",
"react-dom": "/vendor/react-dom@18.2.0/es2022/react-dom.bundle.mjs"
}
}
</script>
<?php tpl_render_styles(null, 'header'); ?>
<style>
.btn{display:inline-flex;align-items:center;gap:.4rem;padding:.35rem .7rem;border-radius:.75rem;border:1px solid #e5e7eb;background:#fff;font-size:.9rem;cursor:pointer;}

View File

@@ -165,3 +165,9 @@ body.page-login {
.gjs-block-category[data-id="Bibliothek"] {
display: none !important;
}
.craft-editor-shell{display:flex;flex-direction:column;height:100%;}
.craft-editor-toolbar{display:flex;gap:.5rem;padding:.5rem;border-bottom:1px solid #e2e8f0;background:#f8fafc;}
.craft-editor-canvas{flex:1;min-height:300px;overflow:auto;padding:12px;border:1px solid #e2e8f0;border-radius:8px;background:#fff;}
.craft-container{min-height:40px;}
.craft-text:focus{outline:2px solid #38bdf8;outline-offset:2px;}

View File

@@ -0,0 +1,251 @@
// assets/js/craft-editor.js
import React from 'react';
import ReactDOM from 'react-dom';
import {
Editor,
Frame,
Element,
useEditor,
useNode,
} from '/vendor/@craftjs/core@0.2.12/X-ZXJlYWN0LHJlYWN0LWRvbQ/es2022/core.bundle.mjs';
const RootCanvas = ({ children }) => {
return React.createElement(
'div',
{ id: 'craftCanvas', className: 'craft-editor-canvas' },
children
);
};
RootCanvas.craft = {
displayName: 'RootCanvas',
props: {},
};
const Container = ({ children }) => {
return React.createElement('div', { className: 'craft-container' }, children);
};
Container.craft = {
displayName: 'Container',
props: {},
};
const Text = ({ text, tag }) => {
const {
connectors: { connect, drag },
actions,
} = useNode();
const Tag = tag || 'div';
return React.createElement(Tag, {
ref: (ref) => ref && connect(drag(ref)),
className: 'craft-text',
'data-craft-text': '1',
contentEditable: true,
suppressContentEditableWarning: true,
onInput: (ev) => {
const html = ev.currentTarget.innerHTML;
actions.setProp((props) => {
props.text = html;
}, 120);
},
dangerouslySetInnerHTML: { __html: text || '' },
});
};
Text.craft = {
displayName: 'Text',
props: {
text: '',
tag: 'div',
},
};
const Toolbar = ({ onAddText, onAddContainer }) => {
return React.createElement(
'div',
{ className: 'craft-editor-toolbar' },
React.createElement('button', { type: 'button', className: 'btn', onClick: onAddText }, 'Text'),
React.createElement('button', { type: 'button', className: 'btn', onClick: onAddContainer }, 'Container')
);
};
const EditorBridge = ({ onReady }) => {
const { actions, query } = useEditor();
React.useEffect(() => {
if (onReady) {
onReady.current = { actions, query };
}
}, [actions, query, onReady]);
return null;
};
const CraftApp = ({ initialData, onReady }) => {
return React.createElement(
'div',
{ className: 'craft-editor-shell' },
React.createElement(Toolbar, {
onAddText: () => {
if (!onReady?.current) return;
const { actions, query } = onReady.current;
const nodeTree = query
.parseReactElement(React.createElement(Text, { text: 'Neuer Text' }))
.toNodeTree();
actions.addNodeTree(nodeTree, 'ROOT');
},
onAddContainer: () => {
if (!onReady?.current) return;
const { actions, query } = onReady.current;
const nodeTree = query
.parseReactElement(React.createElement(Element, { is: Container, canvas: true }))
.toNodeTree();
actions.addNodeTree(nodeTree, 'ROOT');
},
}),
React.createElement(
Editor,
{ resolver: { RootCanvas, Container, Text } },
React.createElement(EditorBridge, { onReady }),
React.createElement(
Frame,
{ data: initialData || undefined },
React.createElement(Element, { is: RootCanvas, canvas: true })
)
)
);
};
const buildSerializedFromHtml = (html) => {
const textId = `text-${Math.random().toString(36).slice(2, 9)}`;
const data = {
ROOT: {
type: { resolvedName: 'RootCanvas' },
isCanvas: true,
props: {},
displayName: 'RootCanvas',
nodes: [textId],
linkedNodes: {},
},
};
data[textId] = {
type: { resolvedName: 'Text' },
isCanvas: false,
props: { text: html || '', tag: 'div' },
displayName: 'Text',
parent: 'ROOT',
nodes: [],
linkedNodes: {},
};
return JSON.stringify(data);
};
const looksSerialized = (payload) => {
if (!payload) return false;
try {
const parsed = typeof payload === 'string' ? JSON.parse(payload) : payload;
return !!(parsed && typeof parsed === 'object' && parsed.ROOT);
} catch {
return false;
}
};
const getCanvasHtml = () => {
const canvas = document.getElementById('craftCanvas');
if (!canvas) return '';
if (canvas.children.length === 1 && canvas.children[0]?.dataset?.craftText === '1') {
return canvas.children[0].innerHTML;
}
return canvas.innerHTML;
};
export function initCraftEditor() {
const container = document.getElementById('craftEditor');
const mount = document.getElementById('craftEditorMount');
if (!container || !mount) return null;
let mounted = false;
let pendingSerialized = null;
const bridgeRef = { current: null };
const ensureMount = (initialData) => {
if (mounted) return;
mounted = true;
if (ReactDOM.createRoot) {
const root = ReactDOM.createRoot(mount);
root.render(React.createElement(CraftApp, { initialData, onReady: bridgeRef }));
} else {
ReactDOM.render(React.createElement(CraftApp, { initialData, onReady: bridgeRef }), mount);
}
};
const applySerialized = (serialized) => {
const api = bridgeRef.current;
if (!api) {
pendingSerialized = serialized;
return;
}
try {
api.actions.deserialize(serialized);
} catch {}
};
const flushPending = () => {
if (pendingSerialized && bridgeRef.current) {
const next = pendingSerialized;
pendingSerialized = null;
applySerialized(next);
return;
}
if (pendingSerialized && !bridgeRef.current) {
setTimeout(flushPending, 60);
}
};
const initWithSerialized = (serialized) => {
if (!mounted) {
ensureMount(serialized ? JSON.parse(serialized) : null);
return;
}
if (serialized) {
applySerialized(serialized);
}
};
return {
show() {
container.classList.remove('hidden');
},
hide() {
container.classList.add('hidden');
},
setContent(html, craftJson) {
const useCraft = looksSerialized(craftJson);
const serialized = useCraft
? String(craftJson)
: buildSerializedFromHtml(html || '');
initWithSerialized(serialized);
flushPending();
},
getContent() {
return getCanvasHtml();
},
getCraftJson() {
const api = bridgeRef.current;
if (!api) return pendingSerialized || '';
try {
return api.query.serialize();
} catch {
return '';
}
},
serializeFromHtml(html) {
return buildSerializedFromHtml(html || '');
},
clear() {
const empty = buildSerializedFromHtml('');
initWithSerialized(empty);
},
focus() {
const canvas = document.getElementById('craftCanvas');
if (canvas) canvas.focus();
},
};
}

View File

@@ -2,16 +2,19 @@
// Öffnen, Befüllen, Speichern (mit Live-HTML), Preview Race-Schutz & Lade-Overlay.
import { apiUpdate, apiList, apiGet, toast, apiAction } from './api.js';
import { initCraftEditor } from './craft-editor.js';
export function initEditor() {
  // ... (Alle Konstanten bleiben unverändert) ...
  const dlg          = document.getElementById('editorDialog');
  const iframe       = document.getElementById('editorFrame');
  const btnSave      = document.getElementById('btn-save');
  const btnPreview   = document.getElementById('btn-preview');
  const btnTest      = document.getElementById('btn-test');
  const btnClose     = document.getElementById('btn-close');
  const btnClear     = document.getElementById('btn-clear-main');
const btnSave = document.getElementById('btn-save');
const btnPreview = document.getElementById('btn-preview');
const btnTest = document.getElementById('btn-test');
const btnClose = document.getElementById('btn-close');
const btnClear = document.getElementById('btn-clear-main');
const editorSelect = document.getElementById('editorTypeSelect');
const craftEditor = initCraftEditor();
  const prevDlg      = document.getElementById('previewDialog');
  const sendDlg      = document.getElementById('sendTestDialog');
@@ -26,11 +29,12 @@ export function initEditor() {
  const prevFrame    = document.getElementById('previewFrame');
  const btnPrevClose = document.getElementById('btn-close-preview');
  let current = null;   // { resource, id, name }
  let bridgeListener = null;
  let reqToken = 0;     // steigender Token pro Öffnen -> ignoriert verspätete Events
  let senderOptions = [];
  let senderLoadPromise = null;
let current = null; // { resource, id, name }
let bridgeListener = null;
let reqToken = 0; // steigender Token pro Öffnen -> ignoriert verspätete Events
let senderOptions = [];
let senderLoadPromise = null;
let currentEditorType = 'grapesjs';
  const ok  = (m) => toast(m, true);
  const err = (m) => toast(m, false);
@@ -69,20 +73,23 @@ export function initEditor() {
    </html>`;
  }
  async function readEditedHtml() {
    const win = iframe?.contentWindow;
    const doc = iframe?.contentDocument;
    if (!win || !doc) return '';
async function readEditedHtml() {
if (currentEditorType === 'craftjs') {
return craftEditor ? craftEditor.getContent() : '';
}
const win = iframe?.contentWindow;
const doc = iframe?.contentDocument;
if (!win || !doc) return '';
    const ed = win.__gjs || (win.grapesjs && win.grapesjs.editors && win.grapesjs.editors[0]) || null;
    if (ed && typeof ed.getHtml === 'function') {
      const html = ed.getHtml();
      const css  = (typeof ed.getCss === 'function') ? ed.getCss() : '';
      return css ? `<style>${css}</style>\n${html}` : html;
    }
    const root = doc.querySelector('#gjs') || doc.body || doc.documentElement;
    return root ? root.innerHTML : '';
  }
const ed = win.__gjs || (win.grapesjs && win.grapesjs.editors && win.grapesjs.editors[0]) || null;
if (ed && typeof ed.getHtml === 'function') {
const html = ed.getHtml();
const css = (typeof ed.getCss === 'function') ? ed.getCss() : '';
return css ? `<style>${css}</style>\n${html}` : html;
}
const root = doc.querySelector('#gjs') || doc.body || doc.documentElement;
return root ? root.innerHTML : '';
}
  function waitForEditor(maxMs = 8000) {
    return new Promise((resolve, reject) => {
@@ -98,7 +105,7 @@ export function initEditor() {
  }
  
  // 🚨 NEUE FUNKTION: Delegiert das Kommando an den Editor im iFrame
  async function delegateCommand(commandName) {
async function delegateCommand(commandName) {
    try {
      const editor = await waitForEditor(3000);
      if (editor.Commands.has(commandName)) {
@@ -114,9 +121,9 @@ export function initEditor() {
      console.error(e);
      return false;
    }
  }
  // ... (hideReadyBadge bleibt unverändert) ...
  function hideReadyBadge(doc) {
}
// ... (hideReadyBadge bleibt unverändert) ...
function hideReadyBadge(doc) {
    if (!doc) return;
    const kill = () => {
      const el = doc.getElementById('badge');
@@ -143,7 +150,40 @@ export function initEditor() {
    setTimeout(() => { kill(); /* hideByText(doc); */ }, 150);
    setTimeout(() => { kill(); /* hideByText(doc); */ }, 500);
    setTimeout(() => { kill(); /* hideByText(doc); */ }, 1200);
  }
}
function extractCraftHtml(craftJson, fallbackHtml) {
if (!craftJson) return fallbackHtml || '';
try {
const parsed = typeof craftJson === 'string' ? JSON.parse(craftJson) : craftJson;
if (parsed && typeof parsed.html === 'string') {
return parsed.html;
}
} catch {}
return fallbackHtml || '';
}
function looksCraftSerialized(payload) {
if (!payload) return false;
try {
const parsed = typeof payload === 'string' ? JSON.parse(payload) : payload;
return !!(parsed && typeof parsed === 'object' && parsed.ROOT);
} catch {
return false;
}
}
function setEditorType(nextType) {
currentEditorType = nextType === 'craftjs' ? 'craftjs' : 'grapesjs';
if (editorSelect) editorSelect.value = currentEditorType;
if (currentEditorType === 'craftjs') {
iframe?.classList?.add('hidden');
craftEditor?.show();
} else {
craftEditor?.hide();
iframe?.classList?.remove('hidden');
}
}
  
  // ... (Lade-Overlay bleibt unverändert) ...
  let veilEl = null;
@@ -328,6 +368,9 @@ export function initEditor() {
let hasJson = false;
let jsonState = '';
let editorType = 'grapesjs';
let craftJson = '';
    await Promise.all([
      (async() => {
try {
@@ -342,12 +385,34 @@ export function initEditor() {
if (!fresh && !looksJson) {
fresh = rawContent;
}
editorType = String(row?.editor_type ?? row?.item?.editor_type ?? 'grapesjs').toLowerCase();
craftJson = row?.craft_json ?? row?.item?.craft_json ?? '';
} catch {}
      })(),
})(),
      (async() => { snippets = await buildSnippetsForContext(current); })(),
      (async() => { refLib   = await buildRefLibForContext(current); })()
    ]);
editorType = editorType === 'craftjs' ? 'craftjs' : 'grapesjs';
setEditorType(editorType);
if (editorType === 'craftjs') {
const craftHtml = extractCraftHtml(craftJson, fresh);
craftEditor?.setContent(craftHtml, craftJson);
hideVeil();
if (dlg && typeof dlg.showModal === 'function') dlg.showModal();
if (!looksCraftSerialized(craftJson) && craftEditor?.serializeFromHtml) {
const seed = craftEditor.serializeFromHtml(craftHtml);
try {
await apiUpdate(current.resource, current.id, {
editor_type: 'craftjs',
html: craftHtml,
craft_json: seed
});
} catch {}
}
return;
}
    // iFrame-Load -> Bridge-Ready abhören
    iframe.onload = function () {
      if (myToken !== reqToken) return;
@@ -417,21 +482,37 @@ export function initEditor() {
    dlg?.showModal?.();
  }
  // ---------- Speichern (DELEGIERT) ----------
  // 🚨 KORRIGIERT: Delegiert Speichern an den iFrame, der die JSON-Daten holt!
  async function save() {
    if (!current?.id) return err('Keine aktive ID');
// ---------- Speichern (DELEGIERT) ----------
// 🚨 KORRIGIERT: Delegiert Speichern an den iFrame, der die JSON-Daten holt!
async function save() {
if (!current?.id) return err('Keine aktive ID');
    return delegateCommand('save-data');
  }
if (currentEditorType === 'craftjs') {
const html = craftEditor ? craftEditor.getContent() : '';
const craftJson = craftEditor && craftEditor.getCraftJson
? craftEditor.getCraftJson()
: JSON.stringify({ html });
const payload = { html, craft_json: craftJson, editor_type: 'craftjs' };
const res = await apiUpdate(current.resource, current.id, payload);
if (res?.ok) ok('Gespeichert');
else err(res?.error || 'Speichern fehlgeschlagen');
return res?.ok;
}
return delegateCommand('save-data');
}
  // ... (Der Rest der Funktionen bleibt unverändert) ...
  async function clearEditor() {
    const win = iframe?.contentWindow;
    const ed  = win?.__gjs || (win?.grapesjs && win.grapesjs.editors && win.grapesjs.editors[0]) || null;
    if (ed) {
      ed.setComponents('');
      ed.setStyle('');
async function clearEditor() {
if (currentEditorType === 'craftjs') {
craftEditor?.clear();
return;
}
const win = iframe?.contentWindow;
const ed = win?.__gjs || (win?.grapesjs && win.grapesjs.editors && win.grapesjs.editors[0]) || null;
if (ed) {
ed.setComponents('');
ed.setStyle('');
    } else {
      writeHtmlToFrame('');
    }
@@ -485,7 +566,7 @@ export function initEditor() {
  }
  function closePreview(){ prevDlg?.close?.(); }
  function close() {
function close() {
    // nächstes Öffnen invalidiert laufende asyncs
    reqToken++;
@@ -503,17 +584,64 @@ export function initEditor() {
    current = null;
    window.__currentItemId = undefined;
    window.__currentEditorCtx = undefined;
  }
}
  // Buttons
  btnSave      && (btnSave.onclick      = save);
  btnClear     && (btnClear.onclick     = clearEditor);
  btnClose     && (btnClose.onclick     = close);
async function switchEditor(nextType) {
if (!current?.id) return;
const target = nextType === 'craftjs' ? 'craftjs' : 'grapesjs';
if (target === currentEditorType) return;
const confirmed = window.confirm('Editor wechseln? Ungespeicherte Änderungen gehen verloren.');
if (!confirmed) {
if (editorSelect) editorSelect.value = currentEditorType;
return;
}
if (currentEditorType === 'grapesjs' && target === 'craftjs') {
const html = await readEditedHtml();
const craftJson = craftEditor && craftEditor.serializeFromHtml
? craftEditor.serializeFromHtml(html)
: JSON.stringify({ html });
const res = await apiUpdate(current.resource, current.id, {
editor_type: 'craftjs',
html,
craft_json: craftJson
});
if (!res?.ok) {
err(res?.error || 'Editorwechsel fehlgeschlagen');
if (editorSelect) editorSelect.value = currentEditorType;
return;
}
setEditorType('craftjs');
craftEditor?.setContent(html, craftJson);
iframe.src = 'about:blank#' + Date.now();
ok('Editor gewechselt');
return;
}
if (currentEditorType === 'craftjs' && target === 'grapesjs') {
const html = craftEditor ? craftEditor.getContent() : '';
const res = await apiUpdate(current.resource, current.id, {
editor_type: 'grapesjs',
html
});
if (!res?.ok) {
err(res?.error || 'Editorwechsel fehlgeschlagen');
if (editorSelect) editorSelect.value = currentEditorType;
return;
}
ok('Editor gewechselt');
await open({ id: current.id, name: current.name }, current.resource);
}
}
// Buttons
btnSave && (btnSave.onclick = save);
btnClear && (btnClear.onclick = clearEditor);
btnClose && (btnClose.onclick = close);
  btnPrevClose && (btnPrevClose.onclick = closePreview);
  btnPreview   && (btnPreview.onclick   = openPreview);
  btnTest      && (btnTest.onclick      = openSend);
  btnCancelSend&& (btnCancelSend.onclick= closeSend);
  sendForm     && (sendForm.onsubmit    = doSend);
btnPreview && (btnPreview.onclick = openPreview);
btnTest && (btnTest.onclick = openSend);
btnCancelSend&& (btnCancelSend.onclick= closeSend);
sendForm && (sendForm.onsubmit = doSend);
editorSelect && (editorSelect.onchange = () => switchEditor(editorSelect.value));
  window.AdminTestSend = window.AdminTestSend || {};
  window.AdminTestSend.open = (opts = {}) => {

View File

@@ -272,6 +272,9 @@ function fillSettingsForm(settings) {
settingsForm.bridge_token.value = settings.bridge_token || '';
settingsForm.sender_token.value = settings.sender_token || '';
settingsForm.external_api_token.value = settings.external_api_token || '';
if (settingsForm.editor_default) {
settingsForm.editor_default.value = settings.editor_default || 'grapesjs';
}
state.rotate = { bridge: false, sender: false, external: false };
refreshAdminTables(settings.bridge_setup?.tables || [], settings.bridge_tables || []);
}
@@ -317,6 +320,7 @@ async function submitSettingsForm(ev) {
bridge_token: settingsForm.bridge_token.value.trim(),
sender_token: settingsForm.sender_token.value.trim(),
external_api_token: settingsForm.external_api_token.value.trim(),
editor_default: settingsForm.editor_default ? settingsForm.editor_default.value : undefined,
bridge_tables: bridgeTables,
rotate_bridge_token: state.rotate.bridge ? 1 : 0,
rotate_sender_token: state.rotate.sender ? 1 : 0,

View File

@@ -61,12 +61,20 @@ require __DIR__ . '/../partials/structure/layout_start.php';
<div class="h-full flex flex-col">
<div class="px-4 py-2 border-b flex items-center gap-2 bg-white/80 backdrop-blur">
<strong class="me-auto">E-Mail Editor</strong>
<label class="text-xs text-slate-600">Editor</label>
<select id="editorTypeSelect" class="input h-8 py-0 text-sm">
<option value="grapesjs">GrapesJS</option>
<option value="craftjs">Craft.js</option>
</select>
<button id="btn-clear-main" type="button" class="btn" title="Leeren">🧹</button>
<button id="btn-preview" type="button" class="btn">Vorschau</button>
<button id="btn-test" type="button" class="btn">Testversand</button>
<button id="btn-save" type="button" class="btn">Speichern</button>
<button id="btn-close" type="button" class="btn">Schließen</button>
</div>
<div id="craftEditor" class="hidden flex-1 w-full">
<div id="craftEditorMount" class="w-full h-full"></div>
</div>
<iframe id="editorFrame" src="about:blank" class="flex-1 w-full"></iframe>
</div>
</dialog>

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -106,6 +106,7 @@ CREATE TABLE IF NOT EXISTS `emailtemplate_customer_settings` (
`bridge_token` varchar(255) DEFAULT NULL,
`sender_token` varchar(255) DEFAULT NULL,
`external_api_token` varchar(255) DEFAULT NULL,
`editor_default` varchar(32) DEFAULT NULL,
`created_at` timestamp NOT NULL DEFAULT current_timestamp(),
`updated_at` timestamp NOT NULL DEFAULT current_timestamp() ON UPDATE current_timestamp(),
PRIMARY KEY (`customer_id`)
@@ -204,6 +205,8 @@ CREATE TABLE IF NOT EXISTS `emailtemplate_snippets` (
`category` varchar(100) NOT NULL DEFAULT '',
`json_content` mediumtext DEFAULT NULL,
`content` mediumtext DEFAULT NULL,
`editor_type` varchar(32) DEFAULT NULL,
`craft_json` mediumtext DEFAULT NULL,
`block_id` int(10) unsigned DEFAULT NULL,
`created_at` timestamp NOT NULL DEFAULT current_timestamp(),
`updated_at` timestamp NOT NULL DEFAULT current_timestamp() ON UPDATE current_timestamp(),
@@ -222,6 +225,8 @@ CREATE TABLE IF NOT EXISTS `emailtemplate_templates` (
`api_name` varchar(190) NOT NULL,
`json_content` mediumtext DEFAULT NULL,
`html` mediumtext DEFAULT NULL,
`editor_type` varchar(32) DEFAULT NULL,
`craft_json` mediumtext DEFAULT NULL,
`created_at` timestamp NOT NULL DEFAULT current_timestamp(),
`updated_at` timestamp NOT NULL DEFAULT current_timestamp() ON UPDATE current_timestamp(),
PRIMARY KEY (`id`),

View File

@@ -453,9 +453,23 @@ class ApiKernel
$topHtml = ($htmlCol && isset($row[$htmlCol])) ? (string)$row[$htmlCol] : null;
$jsonCol = $this->firstExisting($allCols, ['json_content']);
$topContent = ($jsonCol && isset($row[$jsonCol])) ? $row[$jsonCol] : null;
$editorCol = $this->firstExisting($allCols, ['editor_type', 'editor']);
$craftCol = $this->firstExisting($allCols, ['craft_json', 'craft_content', 'craft_data']);
$editorType = $editorCol && isset($row[$editorCol]) ? strtolower((string)$row[$editorCol]) : '';
$pendingUpdate = [];
$gjsComponents = [];
if ($editorCol && $editorType === '') {
$settings = $this->getCustomerSettings((int)($auth['customer_id'] ?? 0));
$editorType = strtolower((string)($settings['editor_default'] ?? 'grapesjs'));
if (!in_array($editorType, ['grapesjs', 'craftjs'], true)) {
$editorType = 'grapesjs';
}
$pendingUpdate[$editorCol] = $editorType;
$rowOut[$editorCol] = $editorType;
}
if ($topContent !== null) {
$decodedContent = json_decode($topContent, true);
if (is_array($decodedContent)) {
@@ -467,6 +481,34 @@ class ApiKernel
$gjsComponents = $this->parseHtmlToGjsComponents($topHtml);
}
if ($editorType === 'grapesjs' && $jsonCol && empty($topContent) && $topHtml !== null) {
$pendingUpdate[$jsonCol] = $this->encodeJson($gjsComponents);
$topContent = $pendingUpdate[$jsonCol];
$rowOut[$jsonCol] = $pendingUpdate[$jsonCol];
}
if ($editorType === 'craftjs' && $craftCol) {
$craftPayload = isset($row[$craftCol]) ? (string)$row[$craftCol] : '';
if ($craftPayload === '') {
$pendingUpdate[$craftCol] = $this->encodeJson(['html' => (string)($topHtml ?? '')]);
$rowOut[$craftCol] = $pendingUpdate[$craftCol];
}
}
if ($pendingUpdate) {
$pendingUpdate[$idCol] = $row[$idCol] ?? $id;
[$tw, $tp] = $this->tenantWhere($auth);
$set = [];
foreach (array_keys($pendingUpdate) as $c) {
if ($c === $idCol) continue;
$set[] = "`$c` = :$c";
}
$sql = "UPDATE `$t` SET " . implode(',', $set) . " WHERE `$idCol` = :$idCol" . $tw . " LIMIT 1";
$stmt = $this->pdo->prepare($sql);
foreach ($pendingUpdate as $k => $v) $stmt->bindValue(":$k", $v);
foreach ($tp as $k => $v) $stmt->bindValue($k, $v);
$stmt->execute();
}
$usage = $this->calculateUsage($kind, (int)$rowOut['id'], $auth);
$this->respond([
@@ -478,6 +520,8 @@ class ApiKernel
'html' => $topHtml,
'content' => $topContent,
'gjs_components' => $gjsComponents,
'editor_type' => $editorType ?: null,
'craft_json' => $craftCol && isset($rowOut[$craftCol]) ? $rowOut[$craftCol] : null,
'usage' => $usage,
]);
}
@@ -510,9 +554,13 @@ class ApiKernel
: ['content_json', 'json', 'content', 'structure_json'];
$json = $this->val($this->in, $jsonKeys, null);
$settings = $this->val($this->in, ['settings_json', 'settings'], null);
$editorType = strtolower(trim((string)$this->val($this->in, ['editor_type', 'editor'], '')));
$craftJson = $this->val($this->in, ['craft_json', 'craft_content', 'craft_data'], null);
$templateId = $this->val($this->in, ['template_id', 'tpl_id'], null);
$sectionId = $this->val($this->in, ['section_id', 'sec_id'], null);
$blockId = $this->val($this->in, ['block_id', 'blk_id'], null);
$editorType = strtolower(trim((string)$this->val($this->in, ['editor_type', 'editor'], '')));
$craftJson = $this->val($this->in, ['craft_json', 'craft_content', 'craft_data'], null);
$data = [$nameCol => $name];
if ($kind === 'templates') {
@@ -541,6 +589,10 @@ class ApiKernel
$htmlDbCol = $this->firstExisting($allCols, ($kind === 'snippets' ? ['content'] : ['html', 'body', 'markup']));
$jsonDbCol = $this->firstExisting($allCols, ['json_content']);
$editorDbCol = $this->firstExisting($allCols, ['editor_type', 'editor']);
$craftDbCol = $this->firstExisting($allCols, ['craft_json', 'craft_content', 'craft_data']);
$editorDbCol = $this->firstExisting($allCols, ['editor_type', 'editor']);
$craftDbCol = $this->firstExisting($allCols, ['craft_json', 'craft_content', 'craft_data']);
// --- LOGIK mit ERWEITERTER PRÜFUNG START ---
@@ -570,6 +622,19 @@ class ApiKernel
}
// --- LOGIK mit ERWEITERTER PRÜFUNG ENDE ---
if ($editorDbCol) {
if ($editorType === '' && in_array($kind, ['templates', 'snippets'], true)) {
$settings = $this->getCustomerSettings((int)($auth['customer_id'] ?? 0));
$editorType = strtolower((string)($settings['editor_default'] ?? 'grapesjs'));
}
if ($editorType !== '' && in_array($editorType, ['grapesjs', 'craftjs'], true)) {
$data[$editorDbCol] = $editorType;
}
}
if ($craftDbCol && $craftJson !== null) {
$data[$craftDbCol] = is_string($craftJson) ? $craftJson : $this->encodeJson($craftJson);
}
$c = $this->firstExisting($allCols, ['settings_json', 'settings']);
if ($c && $settings !== null) $data[$c] = is_string($settings) ? $settings : $this->encodeJson($settings);
@@ -685,6 +750,13 @@ class ApiKernel
}
// --- LOGIK mit ERWEITERTER PRÜFUNG ENDE ---
if ($editorDbCol && $editorType !== '' && in_array($editorType, ['grapesjs', 'craftjs'], true)) {
$data[$editorDbCol] = $editorType;
}
if ($craftDbCol && $craftJson !== null) {
$data[$craftDbCol] = is_string($craftJson) ? $craftJson : $this->encodeJson($craftJson);
}
$c = $this->firstExisting($allCols, ['settings_json', 'settings']);
if ($settings !== null && $c) $data[$c] = is_string($settings) ? $settings : $this->encodeJson($settings);
@@ -1696,6 +1768,7 @@ class ApiKernel
$bridgeToken = trim((string)($this->in['bridge_token'] ?? ''));
$senderToken = trim((string)($this->in['sender_token'] ?? ''));
$externalToken = trim((string)($this->in['external_api_token'] ?? ''));
$editorDefault = strtolower(trim((string)($this->in['editor_default'] ?? '')));
$rotateBridge = !empty($this->in['rotate_bridge_token']);
$rotateSender = !empty($this->in['rotate_sender_token']);
$rotateExternal = !empty($this->in['rotate_external_token']);
@@ -1710,11 +1783,16 @@ class ApiKernel
if ($rotateSender || $senderToken === '') $senderToken = $this->generateToken();
if ($rotateExternal || $externalToken === '') $externalToken = $this->generateToken();
if ($editorDefault !== '' && !in_array($editorDefault, ['grapesjs', 'craftjs'], true)) {
$this->fail('Ungültiger Editor-Typ', null, 422);
}
$settings = $this->saveCustomerSettings($customerId, [
'bridge_url' => $bridgeUrl,
'bridge_token' => $bridgeToken,
'sender_token' => $senderToken,
'external_api_token' => $externalToken,
'editor_default' => $editorDefault ?: null,
'bridge_tables' => $bridgeTables,
]);
@@ -2240,7 +2318,7 @@ class ApiKernel
{
if ($customerId <= 0) return [];
$this->ensureCustomerSettingsTableExists();
$allowed = ['bridge_url', 'bridge_token', 'sender_token', 'external_api_token', 'bridge_tables', 'bridge_setup'];
$allowed = ['bridge_url', 'bridge_token', 'sender_token', 'external_api_token', 'editor_default', 'bridge_tables', 'bridge_setup'];
$fields = array_intersect_key($data, array_flip($allowed));
if (!$fields) return $this->getCustomerSettings($customerId);
if (array_key_exists('bridge_tables', $fields)) {
@@ -2303,6 +2381,9 @@ class ApiKernel
} else {
$row['bridge_setup'] = $this->defaultBridgeSetup();
}
if (empty($row['editor_default'])) {
$row['editor_default'] = 'grapesjs';
}
return $row;
}
@@ -2577,6 +2658,7 @@ CREATE TABLE IF NOT EXISTS `$table` (
`bridge_token` varchar(255) DEFAULT NULL,
`sender_token` varchar(255) DEFAULT NULL,
`external_api_token` varchar(255) DEFAULT NULL,
`editor_default` varchar(32) DEFAULT NULL,
`bridge_tables` text DEFAULT NULL,
`created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
`updated_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
@@ -2615,6 +2697,9 @@ SQL;
if (!in_array('bridge_setup', $columns, true)) {
$missing[] = 'ADD COLUMN `bridge_setup` longtext DEFAULT NULL';
}
if (!in_array('editor_default', $columns, true)) {
$missing[] = 'ADD COLUMN `editor_default` varchar(32) DEFAULT NULL';
}
if (!$missing) {
return;