Upload new version
This commit is contained in:
@@ -110,6 +110,8 @@ $tablesDefaults = [
|
||||
'sections' => 'emailtemplate_sections',
|
||||
'blocks' => 'emailtemplate_blocks',
|
||||
'snippets' => 'emailtemplate_snippets',
|
||||
'content_items' => 'emailtemplate_content_items',
|
||||
'content_sections' => 'emailtemplate_content_sections',
|
||||
];
|
||||
$tables = array_replace($tablesDefaults, $overrides['tables'] ?? []);
|
||||
|
||||
@@ -127,6 +129,8 @@ $columnsDefaults = [
|
||||
'sections' => ['id'=>'id','name'=>'name','cat'=>null,'upd'=>'updated_at'],
|
||||
'blocks' => ['id'=>'id','name'=>'name','cat'=>'category','upd'=>'updated_at'],
|
||||
'snippets' => ['id'=>'id','name'=>'name','cat'=>'category','upd'=>'updated_at'],
|
||||
'content' => ['id'=>'id','name'=>'name','cat'=>'category','upd'=>'updated_at'],
|
||||
'sections_config' => ['id'=>'id','name'=>'name','cat'=>null,'upd'=>'updated_at'],
|
||||
];
|
||||
$columns = array_replace_recursive($columnsDefaults, $overrides['columns'] ?? []);
|
||||
|
||||
|
||||
@@ -83,6 +83,20 @@ require dirname(__DIR__) . '/../structure/layout_start.php';
|
||||
</form>
|
||||
</section>
|
||||
|
||||
<section class="section-card" data-role="admin" id="sectionsManager">
|
||||
<div class="flex items-center justify-between flex-wrap gap-3 mb-3">
|
||||
<div>
|
||||
<h4>Sections verwalten</h4>
|
||||
<p class="text-sm text-slate-600">Die Sortierung steuert, welche Inhalte in anderen Sections eingebunden werden dürfen.</p>
|
||||
</div>
|
||||
</div>
|
||||
<form id="sectionsCreateForm" class="flex flex-wrap gap-2 mb-4">
|
||||
<input type="text" id="sectionNameInput" class="input flex-1 min-w-[220px]" placeholder="Neue Section (Name)" required>
|
||||
<button type="submit" class="btn">Section hinzufügen</button>
|
||||
</form>
|
||||
<ul id="sectionsList" class="space-y-2"></ul>
|
||||
</section>
|
||||
|
||||
<section class="section-card" data-role="admin">
|
||||
<div class="flex items-center justify-between flex-wrap gap-3 mb-3">
|
||||
<div>
|
||||
@@ -253,6 +267,21 @@ require dirname(__DIR__) . '/../structure/layout_start.php';
|
||||
</div>
|
||||
</dialog>
|
||||
|
||||
<dialog id="sectionsDeleteDialog" class="rounded-xl max-w-md w-[90vw] p-5">
|
||||
<form id="sectionsDeleteForm" method="dialog" class="space-y-4">
|
||||
<div class="flex items-center justify-between gap-3 border-b border-slate-200 pb-3">
|
||||
<h3 class="text-lg font-semibold">Section löschen</h3>
|
||||
<button type="button" id="sectionsDeleteCancel" class="btn">Abbrechen</button>
|
||||
</div>
|
||||
<p id="sectionsDeleteText" class="text-sm text-slate-600"></p>
|
||||
<label class="block text-sm text-slate-600">Inhalte verschieben nach</label>
|
||||
<select id="sectionsDeleteTarget" class="input"></select>
|
||||
<div class="flex justify-end gap-2">
|
||||
<button type="submit" class="btn btn-danger">Löschen</button>
|
||||
</div>
|
||||
</form>
|
||||
</dialog>
|
||||
|
||||
<dialog id="configExampleDialog" class="rounded-xl max-w-2xl w-[90vw]">
|
||||
<form method="dialog" class="space-y-3">
|
||||
<h3 class="text-lg font-semibold">Beispiel: Mapping einer Config-Datei</h3>
|
||||
|
||||
@@ -81,11 +81,7 @@ export async function apiAction(
|
||||
* Optional kannst du query-Objekte mitgeben, z.B. { template_id: 123 } für sections.
|
||||
*/
|
||||
export async function apiList(res, query = {}) {
|
||||
const q = new URLSearchParams(query);
|
||||
const qs = q.toString() ? `&${q.toString()}` : "";
|
||||
const r = await apiAction(`${res}.list`, { method: "GET" });
|
||||
// Falls du query serverseitig brauchst (z.B. template_id), nutze eine Action-Variante:
|
||||
// return await apiAction(`${res}.list`, { method:"GET", data: query });
|
||||
const r = await apiAction(`${res}.list`, { method: "GET", data: query });
|
||||
return r?.items ?? [];
|
||||
}
|
||||
|
||||
|
||||
@@ -74,29 +74,9 @@ async function handleEditorMessages(ev) {
|
||||
if (msg.source !== 'email-editor' || msg.type !== 'save') return;
|
||||
|
||||
try {
|
||||
const ctx = window.__currentEditorCtx || {};
|
||||
const id = ctx.id;
|
||||
const mode = (ctx.mode || msg.mode || '').toLowerCase();
|
||||
const refs = Array.isArray(msg.refs) ? msg.refs : [];
|
||||
if (!id || !mode) return;
|
||||
|
||||
if (mode === 'templates') {
|
||||
await fetch('./api.php?resource=template_items&action=sync', {
|
||||
method: 'POST',
|
||||
credentials: 'include',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ template_id: id, items: refs })
|
||||
});
|
||||
} else if (mode === 'sections') {
|
||||
await fetch('./api.php?resource=section_items&action=sync', {
|
||||
method: 'POST',
|
||||
credentials: 'include',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ section_id: id, items: refs.filter(r => r.ref_type === 'block') })
|
||||
});
|
||||
}
|
||||
return;
|
||||
} catch (e) {
|
||||
console.error('refs sync failed', e);
|
||||
console.error('refs sync skipped', e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
const requestedMode = (qs.get('mode') || 'templates').toLowerCase();
|
||||
B.EDITOR_MODE = (B.EDITOR_MODE || requestedMode.toUpperCase());
|
||||
const EDITOR_MODE = (B.EDITOR_MODE || 'TEMPLATES').toLowerCase();
|
||||
const SECTION_ID = Number(qs.get('section_id') || B.CURRENT_SECTION_ID || 0);
|
||||
log(`START: SKRIPT-AUSFÜHRUNG GESTARTET. Editor Modus: ${EDITOR_MODE}.`, '#DC143C');
|
||||
|
||||
const TARGET_CAT_ID = 'custom';
|
||||
@@ -479,7 +480,7 @@
|
||||
jsonProjectDataRaw = '';
|
||||
}
|
||||
|
||||
const resource = EDITOR_MODE;
|
||||
const resource = 'content';
|
||||
const action = `${resource}.update`;
|
||||
const debugSave = (() => {
|
||||
try {
|
||||
@@ -501,6 +502,9 @@
|
||||
// 🚨 KRITISCH: Server erwartet das Feld 'json'
|
||||
json: jsonProjectDataRaw,
|
||||
};
|
||||
if (SECTION_ID) {
|
||||
dataToSend.section_id = SECTION_ID;
|
||||
}
|
||||
if (debugSave) {
|
||||
dataToSend.debug = 1;
|
||||
console.log('[ET DEBUG] save-data payload', {
|
||||
|
||||
@@ -61,28 +61,32 @@
|
||||
};
|
||||
|
||||
const shouldLoad = (resource) => {
|
||||
if (Array.isArray(B.ALLOWED_SECTION_SLUGS) && B.ALLOWED_SECTION_SLUGS.length) {
|
||||
return B.ALLOWED_SECTION_SLUGS.includes(resource);
|
||||
}
|
||||
const mode = (B.EDITOR_MODE || 'TEMPLATES').toUpperCase();
|
||||
|
||||
// HINWEIS: Hier muss für neue Ressourcen (wie 'products') ggf. der mode angepasst werden,
|
||||
// falls sie nicht in TEMPLATES geladen werden sollen.
|
||||
switch (mode) {
|
||||
case 'TEMPLATES':
|
||||
const templateResources = ['templates', 'sections', 'blocks', 'snippets', 'products']; // Beispiel: products hinzugefügt
|
||||
return templateResources.includes(resource);
|
||||
|
||||
case 'SECTIONS':
|
||||
const sectionResources = ['blocks', 'snippets'];
|
||||
return sectionResources.includes(resource);
|
||||
|
||||
case 'BLOCKS':
|
||||
return resource === 'snippets';
|
||||
|
||||
return true;
|
||||
default:
|
||||
log('MODE WARN', `Unbekannter Editor Modus '${mode}' festgestellt.`, 'orange', 'warn');
|
||||
return resource === 'snippets';
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
const fetchSectionsConfig = async () => {
|
||||
const rows = await fetchData('sections_config', 'list');
|
||||
return Array.isArray(rows) ? rows : [];
|
||||
};
|
||||
|
||||
const resolveAllowedSections = async () => {
|
||||
const sections = await fetchSectionsConfig();
|
||||
if (!sections.length) return [];
|
||||
const modeSlug = String(B.EDITOR_MODE || '').toLowerCase();
|
||||
const current = sections.find(s => String(s.slug || '').toLowerCase() === modeSlug);
|
||||
if (!current) return sections;
|
||||
return sections.filter(s => Number(s.position) > Number(current.position));
|
||||
};
|
||||
|
||||
|
||||
const fetchData = (resource, action='list', params = {}) => {
|
||||
// ... (Rest der fetchData-Funktion bleibt unverändert, nutzt aber die korrigierte buildApiUrl)
|
||||
@@ -151,13 +155,13 @@
|
||||
// --- Exportierte Core-Funktionen (jetzt generisch) ---
|
||||
|
||||
// NEU: Generische Fetch-Funktion für jeden Ressourcentyp ('kind')
|
||||
B.fetchResource = (kind) => {
|
||||
B.fetchResource = (kind) => {
|
||||
if (!shouldLoad(kind)) {
|
||||
log('BLOCKED', `Blockiert: ${kind} (Modus: ${B.EDITOR_MODE})`, '#708090', 'info');
|
||||
return Promise.resolve([]);
|
||||
}
|
||||
return fetchData(kind).then(items => Array.isArray(items) ? items : []);
|
||||
};
|
||||
};
|
||||
|
||||
// Die alten hardcodierten Funktionen verwenden jetzt die neue generische Funktion
|
||||
B.fetchTemplates = () => B.fetchResource('templates');
|
||||
@@ -173,37 +177,40 @@
|
||||
};
|
||||
|
||||
// 🚀 Zentrale Funktion zum Laden und Registrieren der Blöcke
|
||||
B.loadAndRegisterApiBlocks = (editor) => {
|
||||
B.loadAndRegisterApiBlocks = (editor) => {
|
||||
const bm = editor.BlockManager;
|
||||
|
||||
// NEU: Ressourcen-Kinds aus der Konfiguration sammeln
|
||||
const resourceKindsToLoad = Object.keys(B.RESOURCE_API_BASES || {});
|
||||
|
||||
if (resourceKindsToLoad.length === 0) {
|
||||
log('FEHLER', 'Keine Ressourcen-Kind-Konfiguration (B.RESOURCE_API_BASES) gefunden.', '#dc3545', 'error', true);
|
||||
bm.remove(PLACEHOLDER_ID);
|
||||
return;
|
||||
}
|
||||
|
||||
// Map aller Fetch-Promises erstellen
|
||||
const fetchPromises = resourceKindsToLoad.map(kind =>
|
||||
B.fetchResource(kind).then(items => items.map(i => ({ ...i, kind: kind })))
|
||||
const loadDynamic = async () => {
|
||||
const sections = await resolveAllowedSections();
|
||||
B.ALLOWED_SECTION_SLUGS = sections.map(s => String(s.slug || '').toLowerCase());
|
||||
if (!sections.length) return [];
|
||||
const promises = sections.map(section =>
|
||||
fetchData('content', 'list', { section_id: section.id })
|
||||
.then(items => (Array.isArray(items) ? items : []).map(i => ({
|
||||
...i,
|
||||
kind: String(section.slug || '').toLowerCase(),
|
||||
section_name: section.name || '',
|
||||
})))
|
||||
);
|
||||
const results = await Promise.all(promises);
|
||||
return results.flat();
|
||||
};
|
||||
|
||||
const startLoad = (B.USE_DYNAMIC_SECTIONS ? loadDynamic() : Promise.resolve([]));
|
||||
|
||||
log('API START', `Starte Promise.all für API-Abruf der Blöcke/Sektionen (${resourceKindsToLoad.join(', ')})...`, '#1E90FF');
|
||||
log('API START', 'Starte API-Abruf für dynamische Sections...', '#1E90FF');
|
||||
|
||||
Promise.all(fetchPromises)
|
||||
.then(results => {
|
||||
const apiItems = results.flat().filter(item => item && item.id);
|
||||
startLoad
|
||||
.then(apiItems => {
|
||||
const filtered = Array.isArray(apiItems) ? apiItems.filter(item => item && item.id) : [];
|
||||
|
||||
log(`API SUCCESS`, `${apiItems.length} Elemente gefunden.`, '#9400D3');
|
||||
logApiData(apiItems);
|
||||
log(`API SUCCESS`, `${filtered.length} Elemente gefunden.`, '#9400D3');
|
||||
logApiData(filtered);
|
||||
|
||||
if (apiItems.length === 0) {
|
||||
if (filtered.length === 0) {
|
||||
log('NO DATA', 'Keine API-Daten gefunden.', 'orange', 'warn', true);
|
||||
} else {
|
||||
apiItems.forEach(item => {
|
||||
filtered.forEach(item => {
|
||||
const blockId = `lib-${item.kind}-${item.id}`;
|
||||
const label = item.name || item.label || 'Unbenannter Block';
|
||||
const itemKindUpper = item.kind.toUpperCase();
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import { apiList, apiCreate, toast } from './api.js';
|
||||
import { apiAction, toast } from './api.js';
|
||||
export function initCreate(){
|
||||
const btn=document.getElementById('btn-new'), dlg=document.getElementById('createDialog'), form=document.getElementById('createForm'), fields=document.getElementById('createFields'), hint=document.getElementById('createHint');
|
||||
if(!btn||!dlg||!form||!fields) return;
|
||||
const curTab=()=>{ const a=document.querySelector('nav [data-tab].bg-sky-50')||document.querySelector('nav [data-tab]'); return a?a.getAttribute('data-tab'):'templates'; };
|
||||
const normalizeApiName=(v='')=>{
|
||||
return String(v)
|
||||
.trim()
|
||||
@@ -14,7 +13,13 @@ export function initCreate(){
|
||||
};
|
||||
|
||||
btn.onclick = async ()=>{
|
||||
fields.innerHTML=''; const tab=curTab();
|
||||
const section = window.__activeSection || null;
|
||||
if (!section) {
|
||||
toast('Bitte zuerst eine Section auswählen', false);
|
||||
return;
|
||||
}
|
||||
const isTemplate = Number(section.is_template) === 1;
|
||||
fields.innerHTML='';
|
||||
const name=document.createElement('input'); name.type='text'; name.required=true; name.placeholder='Name*'; name.className='w-full border rounded-lg px-3 py-2'; name.id='f-name'; fields.appendChild(name);
|
||||
let apiName = null;
|
||||
let apiTouched = false;
|
||||
@@ -31,7 +36,7 @@ export function initCreate(){
|
||||
fields.appendChild(editorSelect);
|
||||
};
|
||||
|
||||
if(tab==='templates'){
|
||||
if(isTemplate){
|
||||
apiName=document.createElement('input');
|
||||
apiName.type='text';
|
||||
apiName.required=true;
|
||||
@@ -49,25 +54,27 @@ export function initCreate(){
|
||||
if (!apiTouched) apiName.value = normalizeApiName(name.value);
|
||||
});
|
||||
}
|
||||
async function addSel(id,label,res){ const sel=document.createElement('select'); sel.id=id; sel.className='w-full border rounded-lg px-3 py-2'; sel.innerHTML=`<option value="">(ohne ${label}-Zuordnung)</option>`; const data=await apiList(res); (data||[]).forEach(t=>{ const o=document.createElement('option'); o.value=t.id; o.textContent=`#${t.id} · ${t.name||''}`; sel.appendChild(o); }); fields.appendChild(sel); }
|
||||
if(tab==='sections') await addSel('f-template','Template','templates');
|
||||
if(tab==='blocks') await addSel('f-section','Section','sections');
|
||||
if(tab==='snippets') await addSel('f-block','Block','blocks');
|
||||
if (tab !== 'templates') addEditorSelect();
|
||||
hint.textContent=`Neues ${tab} anlegen`; dlg.showModal();
|
||||
if (!isTemplate) addEditorSelect();
|
||||
hint.textContent=`Neues Element in ${section.name}`; dlg.showModal();
|
||||
|
||||
form.onsubmit=async(e)=>{ e.preventDefault();
|
||||
const payload={ name:(document.getElementById('f-name')?.value||'').trim() }; if(!payload.name) return;
|
||||
if(tab==='templates') {
|
||||
if(isTemplate) {
|
||||
payload.api_name=(document.getElementById('f-api-name')?.value||'').trim();
|
||||
if(!payload.api_name) return;
|
||||
}
|
||||
payload.editor_type=(document.getElementById('f-editor-type')?.value||'grapesjs');
|
||||
if(tab==='snippets') payload.content=''; else payload.html='';
|
||||
if(tab==='sections') payload.template_id=document.getElementById('f-template')?.value||null;
|
||||
if(tab==='blocks') payload.section_id =document.getElementById('f-section')?.value ||null;
|
||||
if(tab==='snippets') payload.block_id =document.getElementById('f-block')?.value ||null;
|
||||
const r=await apiCreate(tab,payload); if(r&&r.id){ dlg.close(); toast('Erstellt',true); window.loadList && window.loadList(tab); } else { toast('Erstellen fehlgeschlagen',false,{duration:3000}); console.error('Create failed',r); }
|
||||
payload.html='';
|
||||
payload.section_id = section.id;
|
||||
const r=await apiAction('content.create', { method:'POST', data: payload });
|
||||
if(r&&r.id){
|
||||
dlg.close();
|
||||
toast('Erstellt',true);
|
||||
window.loadList && window.loadList(section);
|
||||
} else {
|
||||
toast('Erstellen fehlgeschlagen',false,{duration:3000});
|
||||
console.error('Create failed',r);
|
||||
}
|
||||
};
|
||||
};
|
||||
const cancel=document.getElementById('createCancel'); cancel && (cancel.onclick=()=>dlg.close());
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/* /assets/js/ui-editor.js (KORRIGIERT: Speichern wird an iFrame-Editor delegiert) */
|
||||
// Öffnen, Befüllen, Speichern (mit Live-HTML), Preview – Race-Schutz & Lade-Overlay.
|
||||
|
||||
import { apiUpdate, apiList, apiGet, toast, apiAction } from './api.js';
|
||||
import { apiUpdate, toast, apiAction } from './api.js';
|
||||
import { initCraftEditor } from './craft-editor.js';
|
||||
|
||||
export function initEditor() {
|
||||
@@ -29,7 +29,7 @@ export function initEditor() {
|
||||
const prevFrame = document.getElementById('previewFrame');
|
||||
const btnPrevClose = document.getElementById('btn-close-preview');
|
||||
|
||||
let current = null; // { resource, id, name }
|
||||
let current = null; // { resource, id, name, section }
|
||||
let bridgeListener = null;
|
||||
let reqToken = 0; // steigender Token pro Öffnen -> ignoriert verspätete Events
|
||||
let senderOptions = [];
|
||||
@@ -40,12 +40,12 @@ export function initEditor() {
|
||||
const err = (m) => toast(m, false);
|
||||
|
||||
// ---------- Hilfen ----------
|
||||
function activeMode() {
|
||||
const b = document.querySelector('nav [data-tab].bg-sky-50, nav [data-tab].text-sky-700, nav [data-tab].active');
|
||||
return (b?.dataset?.tab) || (current?.resource) || 'templates';
|
||||
}
|
||||
function activeMode() {
|
||||
const activeSection = window.__activeSection || current?.section || null;
|
||||
return (activeSection?.slug) || (current?.resource) || 'emailtemplate';
|
||||
}
|
||||
|
||||
function setSendContext(id, name = '') {
|
||||
function setSendContext(id, name = '') {
|
||||
if (sendDlg) {
|
||||
if (id) {
|
||||
sendDlg.dataset.templateId = String(id);
|
||||
@@ -59,12 +59,12 @@ export function initEditor() {
|
||||
sendInfo.textContent = 'Kein Template ausgewählt.';
|
||||
sendInfo.classList.add('text-rose-600');
|
||||
} else {
|
||||
const label = name ? `${name} – Template #${id}` : `Template #${id}`;
|
||||
sendInfo.textContent = label;
|
||||
sendInfo.classList.remove('text-rose-600');
|
||||
}
|
||||
}
|
||||
}
|
||||
const label = name ? `${name} – Template #${id}` : `Template #${id}`;
|
||||
sendInfo.textContent = label;
|
||||
sendInfo.classList.remove('text-rose-600');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function writeHtmlToFrame(html) {
|
||||
iframe.srcdoc = `<!doctype html><html>
|
||||
@@ -210,67 +210,13 @@ export function initEditor() {
|
||||
function showVeil(){ ensureVeil().style.display = 'flex'; }
|
||||
function hideVeil(){ if (veilEl) veilEl.style.display = 'none'; }
|
||||
|
||||
// ... (Kontext-Filter-Ladung bleibt unverändert) ...
|
||||
async function listBlocksForTemplate(templateId){
|
||||
try {
|
||||
const direct = await apiList('blocks', { template_id: templateId });
|
||||
if (Array.isArray(direct) && direct.length) return direct;
|
||||
} catch {}
|
||||
const sections = await apiList('sections', { template_id: templateId }).catch(()=>[]);
|
||||
const out = [];
|
||||
for (const s of (sections || [])) {
|
||||
const b = await apiList('blocks', { section_id: s.id }).catch(()=>[]);
|
||||
if (b?.length) out.push(...b);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
// Snippets eines Templates (direkt oder via Sections->Blocks als Fallback)
|
||||
async function listSnippetsForTemplate(templateId){
|
||||
try {
|
||||
const direct = await apiList('snippets', { template_id: templateId });
|
||||
if (Array.isArray(direct) && direct.length) return direct;
|
||||
} catch {}
|
||||
const sections = await apiList('sections', { template_id: templateId }).catch(()=>[]);
|
||||
const blocksAll = [];
|
||||
for (const s of (sections || [])) {
|
||||
const b = await apiList('blocks', { section_id: s.id }).catch(()=>[]);
|
||||
if (b?.length) blocksAll.push(...b);
|
||||
}
|
||||
const out = [];
|
||||
for (const b of blocksAll) {
|
||||
const sn = await apiList('snippets', { block_id: b.id }).catch(()=>[]);
|
||||
if (sn?.length) out.push(...sn);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
// Referenz-Bibliothek (für „Custom – Fix“)
|
||||
async function buildRefLibForContext(ctx){
|
||||
const kind = (ctx.resource || 'templates').replace(/s$/,''); // template|section|block
|
||||
const id = ctx.id;
|
||||
if (kind === 'template'){
|
||||
const [sections, blocks] = await Promise.all([
|
||||
apiList('sections', { template_id: id }).catch(()=>[]),
|
||||
listBlocksForTemplate(id)
|
||||
]);
|
||||
return { sections, blocks };
|
||||
}
|
||||
if (kind === 'section'){
|
||||
const blocks = await apiList('blocks', { section_id: id }).catch(()=>[]);
|
||||
return { sections: [], blocks };
|
||||
}
|
||||
return { sections: [], blocks: [] }; // block -> keine Sections/Blocks in Fix
|
||||
}
|
||||
// Snippets (für „Custom – Flex“) kontextabhängig
|
||||
async function buildSnippetsForContext(ctx){
|
||||
const kind = (ctx.resource || 'templates').replace(/s$/,'');
|
||||
const id = ctx.id;
|
||||
let rows = [];
|
||||
if (kind === 'template') rows = await listSnippetsForTemplate(id);
|
||||
else if (kind === 'section') rows = await apiList('snippets', { section_id: id }).catch(()=>[]);
|
||||
else if (kind === 'block') rows = await apiList('snippets', { block_id: id }).catch(()=>[]);
|
||||
else rows = await apiList('snippets').catch(()=>[]);
|
||||
return (rows || []).map(r => ({ id: r.id, name: r.name, html: r.content || r.html || '' }));
|
||||
}
|
||||
async function buildRefLibForContext() {
|
||||
return { sections: [], blocks: [] };
|
||||
}
|
||||
|
||||
async function buildSnippetsForContext() {
|
||||
return [];
|
||||
}
|
||||
|
||||
async function loadSenderOptions(force = false) {
|
||||
if (!sendSender) return;
|
||||
@@ -338,19 +284,22 @@ export function initEditor() {
|
||||
}
|
||||
|
||||
// ---------- Öffnen ----------
|
||||
async function open(item, resource) {
|
||||
async function open(item, resource, sectionOverride) {
|
||||
const section = item?.section || sectionOverride || window.__activeSection || null;
|
||||
current = {
|
||||
resource: String(resource || activeMode() || 'templates').toLowerCase(),
|
||||
resource: 'content',
|
||||
id: Number(item?.id || 0),
|
||||
name: item?.name || ''
|
||||
name: item?.name || '',
|
||||
section: section,
|
||||
};
|
||||
if (!current.id) return err('Ungültige ID');
|
||||
if (!current.section) return err('Section nicht gefunden');
|
||||
|
||||
// globaler Kontext
|
||||
window.__currentItemId = current.id;
|
||||
window.__currentEditorCtx = { id: current.id, mode: current.resource };
|
||||
setSendContext(current.id, current.name);
|
||||
if (btnTest) btnTest.classList.toggle('hidden', current.resource !== 'templates');
|
||||
window.__currentEditorCtx = { id: current.id, mode: current.section.slug, section: current.section };
|
||||
setSendContext(current.section?.is_template ? current.id : 0, current.name);
|
||||
if (btnTest) btnTest.classList.toggle('hidden', !current.section?.is_template);
|
||||
|
||||
// Neuen Token erzeugen & alten Listener entfernen
|
||||
reqToken++;
|
||||
@@ -374,7 +323,7 @@ export function initEditor() {
|
||||
await Promise.all([
|
||||
(async() => {
|
||||
try {
|
||||
const row = await apiGet(current.resource, current.id);
|
||||
const row = await apiAction('content.get', { method: 'GET', data: { id: current.id, section_id: current.section.id } });
|
||||
const rawContent = row?.content ?? row?.item?.content ?? '';
|
||||
const trimmed = String(rawContent || '').trim();
|
||||
const looksJson = trimmed.startsWith('{') || trimmed.startsWith('[');
|
||||
@@ -403,10 +352,11 @@ export function initEditor() {
|
||||
if (!looksCraftSerialized(craftJson) && craftEditor?.serializeFromHtml) {
|
||||
const seed = craftEditor.serializeFromHtml(craftHtml);
|
||||
try {
|
||||
await apiUpdate(current.resource, current.id, {
|
||||
await apiUpdate('content', current.id, {
|
||||
editor_type: 'craftjs',
|
||||
html: craftHtml,
|
||||
craft_json: seed
|
||||
craft_json: seed,
|
||||
section_id: current.section.id,
|
||||
});
|
||||
} catch {}
|
||||
}
|
||||
@@ -436,7 +386,7 @@ export function initEditor() {
|
||||
ok('Gespeichert');
|
||||
try {
|
||||
if (typeof window.reloadActiveList === 'function') window.reloadActiveList();
|
||||
else if (typeof window.__reloadList === 'function') window.__reloadList(current.resource);
|
||||
else if (typeof window.__reloadList === 'function') window.__reloadList(current.section);
|
||||
} catch {}
|
||||
return;
|
||||
}
|
||||
@@ -444,7 +394,7 @@ export function initEditor() {
|
||||
// neue Bridge meldet gjs:ready; ältere evtl. core-ready/bridge:ready
|
||||
if (d.type === 'gjs:ready' || d.type === 'core-ready' || d.type === 'bridge:ready' || d.type === 'bridge:booted') {
|
||||
pushInitialHtmlToEditor({
|
||||
mode: current.resource,
|
||||
mode: current.section.slug,
|
||||
html: fresh,
|
||||
snippets,
|
||||
ref: {
|
||||
@@ -462,7 +412,7 @@ export function initEditor() {
|
||||
// Fallback, falls kein Ready ankommt
|
||||
setTimeout(() => {
|
||||
pushInitialHtmlToEditor({
|
||||
mode: current.resource,
|
||||
mode: current.section.slug,
|
||||
html: fresh,
|
||||
snippets,
|
||||
ref: {
|
||||
@@ -474,10 +424,10 @@ export function initEditor() {
|
||||
json: jsonState
|
||||
});
|
||||
}, 1200);
|
||||
};
|
||||
};
|
||||
|
||||
// Jetzt den Editor-Core laden (erst NACH about:blank)
|
||||
iframe.src = `editor/editor-core.php?mode=${encodeURIComponent(current.resource)}&id=${current.id}&t=${Date.now()}`;
|
||||
iframe.src = `editor/editor-core.php?mode=${encodeURIComponent(current.section.slug)}&id=${current.id}§ion_id=${current.section.id}&t=${Date.now()}`;
|
||||
|
||||
dlg?.showModal?.();
|
||||
}
|
||||
@@ -492,8 +442,8 @@ export function initEditor() {
|
||||
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);
|
||||
const payload = { html, craft_json: craftJson, editor_type: 'craftjs', section_id: current.section.id };
|
||||
const res = await apiUpdate('content', current.id, payload);
|
||||
if (res?.ok) ok('Gespeichert');
|
||||
else err(res?.error || 'Speichern fehlgeschlagen');
|
||||
return res?.ok;
|
||||
@@ -525,7 +475,11 @@ export function initEditor() {
|
||||
}
|
||||
|
||||
async function openSend(ctx = null) {
|
||||
const ctxId = ctx?.id ? Number(ctx.id) : (window.__currentItemId || current?.id || 0);
|
||||
if (!current?.section?.is_template) {
|
||||
err('Kein Template geladen');
|
||||
return;
|
||||
}
|
||||
const ctxId = ctx?.id ? Number(ctx.id) : (window.__currentItemId || current?.id || 0);
|
||||
if (!ctxId) {
|
||||
err('Kein Template geladen');
|
||||
return;
|
||||
@@ -600,10 +554,11 @@ export function initEditor() {
|
||||
const craftJson = craftEditor && craftEditor.serializeFromHtml
|
||||
? craftEditor.serializeFromHtml(html)
|
||||
: JSON.stringify({ html });
|
||||
const res = await apiUpdate(current.resource, current.id, {
|
||||
const res = await apiUpdate('content', current.id, {
|
||||
editor_type: 'craftjs',
|
||||
html,
|
||||
craft_json: craftJson
|
||||
craft_json: craftJson,
|
||||
section_id: current.section.id,
|
||||
});
|
||||
if (!res?.ok) {
|
||||
err(res?.error || 'Editorwechsel fehlgeschlagen');
|
||||
@@ -618,9 +573,10 @@ export function initEditor() {
|
||||
}
|
||||
if (currentEditorType === 'craftjs' && target === 'grapesjs') {
|
||||
const html = craftEditor ? craftEditor.getContent() : '';
|
||||
const res = await apiUpdate(current.resource, current.id, {
|
||||
const res = await apiUpdate('content', current.id, {
|
||||
editor_type: 'grapesjs',
|
||||
html
|
||||
html,
|
||||
section_id: current.section.id,
|
||||
});
|
||||
if (!res?.ok) {
|
||||
err(res?.error || 'Editorwechsel fehlgeschlagen');
|
||||
@@ -628,7 +584,7 @@ export function initEditor() {
|
||||
return;
|
||||
}
|
||||
ok('Editor gewechselt');
|
||||
await open({ id: current.id, name: current.name }, current.resource);
|
||||
await open({ id: current.id, name: current.name, section: current.section }, 'content', current.section);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -643,17 +599,21 @@ export function initEditor() {
|
||||
sendForm && (sendForm.onsubmit = doSend);
|
||||
editorSelect && (editorSelect.onchange = () => switchEditor(editorSelect.value));
|
||||
|
||||
window.AdminTestSend = window.AdminTestSend || {};
|
||||
window.AdminTestSend.open = (opts = {}) => {
|
||||
const targetId = Number(opts.id || window.__currentItemId || 0);
|
||||
if (!targetId) {
|
||||
err('Testversand: Keine ID vorhanden');
|
||||
return;
|
||||
}
|
||||
window.__currentItemId = targetId;
|
||||
setSendContext(targetId, opts.name || '');
|
||||
openSend({ id: targetId, name: opts.name || '' });
|
||||
};
|
||||
window.AdminTestSend = window.AdminTestSend || {};
|
||||
window.AdminTestSend.open = (opts = {}) => {
|
||||
const targetId = Number(opts.id || window.__currentItemId || 0);
|
||||
if (!targetId) {
|
||||
err('Testversand: Keine ID vorhanden');
|
||||
return;
|
||||
}
|
||||
if (!current?.section?.is_template) {
|
||||
err('Kein Template geladen');
|
||||
return;
|
||||
}
|
||||
window.__currentItemId = targetId;
|
||||
setSendContext(targetId, opts.name || '');
|
||||
openSend({ id: targetId, name: opts.name || '' });
|
||||
};
|
||||
|
||||
// Public API
|
||||
window.EditorUI = { open, save, close, clear: clearEditor, preview: openPreview };
|
||||
|
||||
@@ -1,18 +1,4 @@
|
||||
import { apiList, apiGet, apiDelete, apiUpdate, apiAction, toast } from './api.js';
|
||||
|
||||
function formatUsage(usage){
|
||||
if (!usage || !usage.total) return '';
|
||||
const parts=[];
|
||||
if (usage.templates) parts.push(`${usage.templates} Template${usage.templates!==1?'s':''}`);
|
||||
if (usage.sections) parts.push(`${usage.sections} Section${usage.sections!==1?'s':''}`);
|
||||
if (usage.blocks) parts.push(`${usage.blocks} Block${usage.blocks!==1?'s':''}`);
|
||||
if (usage.snippets) parts.push(`${usage.snippets} Snippet${usage.snippets!==1?'s':''}`);
|
||||
if (!parts.length) return '';
|
||||
return `<div class="mt-3 text-sm text-rose-600">
|
||||
Dieses Element wird aktuell verwendet in: <strong>${parts.join(', ')}</strong>.<br>
|
||||
Das Löschen entfernt diese Referenzen.
|
||||
</div>`;
|
||||
}
|
||||
import { apiAction, apiUpdate, toast } from './api.js';
|
||||
|
||||
function esc(s = '') {
|
||||
return String(s)
|
||||
@@ -33,25 +19,35 @@ function normalizeApiName(v=''){
|
||||
.replace(/^-|-$/g, '');
|
||||
}
|
||||
|
||||
async function openSnippetEditor(id){
|
||||
let resp = {};
|
||||
try { resp = await apiGet('snippets', id) || {}; } catch(e){}
|
||||
const row = resp?.item || resp?.data || resp || {};
|
||||
const name = row?.name || '';
|
||||
async function fetchContentList(sectionId) {
|
||||
const res = await apiAction('content.list', { method: 'GET', data: { section_id: sectionId } });
|
||||
return Array.isArray(res?.items) ? res.items : [];
|
||||
}
|
||||
|
||||
async function fetchContentItem(id, sectionId) {
|
||||
return await apiAction('content.get', { method: 'GET', data: { id, section_id: sectionId } });
|
||||
}
|
||||
|
||||
async function openContentEditor(item, section) {
|
||||
const id = Number(item?.id || 0);
|
||||
const name = item?.name || '';
|
||||
if (!id) return;
|
||||
const detail = await fetchContentItem(id, section.id).catch(() => ({}));
|
||||
const html = detail?.html ?? detail?.item?.html ?? detail?.content ?? '';
|
||||
|
||||
window.__currentItemId = id;
|
||||
window.__currentEditorCtx = { id, mode: section.slug, section };
|
||||
|
||||
if (window.EditorUI && typeof window.EditorUI.open === 'function') {
|
||||
window.EditorUI.open({ id: Number(id), name }, 'snippets');
|
||||
return;
|
||||
}
|
||||
if (window.__openEditor) {
|
||||
window.__openEditor({ resource: 'snippets', id: Number(id), name });
|
||||
return;
|
||||
}
|
||||
console.warn('Kein Editor-Entry-Point gefunden (EditorUI.open / __openEditor).');
|
||||
window.EditorUI.open({ id, name, html, section }, 'content');
|
||||
} else if (window.__openEditor) {
|
||||
window.__openEditor({ resource: 'content', id, name, html, section });
|
||||
} else {
|
||||
toast('Editor ist nicht initialisiert.', false);
|
||||
}
|
||||
}
|
||||
|
||||
async function openTemplateEditor(id){
|
||||
async function openTemplateEditor(item, section) {
|
||||
const dlg = document.getElementById('editTemplateDialog');
|
||||
const form = document.getElementById('editTemplateForm');
|
||||
const inpName = document.getElementById('edit_tpl_name');
|
||||
@@ -59,9 +55,8 @@ async function openTemplateEditor(id){
|
||||
const apiWarn = document.getElementById('edit_tpl_api_warn');
|
||||
const btnCancel = document.getElementById('editTemplateCancel');
|
||||
|
||||
let resp = {};
|
||||
try { resp = await apiGet('templates', id) || {}; } catch(e){}
|
||||
const row = resp?.item || resp?.data || resp || {};
|
||||
const detail = await fetchContentItem(item.id, section.id).catch(() => ({}));
|
||||
const row = detail?.item || detail?.data || detail || {};
|
||||
const initialApi = row.api_name || '';
|
||||
|
||||
if (inpName) inpName.value = row.name || '';
|
||||
@@ -86,15 +81,16 @@ async function openTemplateEditor(id){
|
||||
async function onSubmit(ev) {
|
||||
ev.preventDefault();
|
||||
try {
|
||||
const res = await apiUpdate('templates', id, {
|
||||
const res = await apiUpdate('content', item.id, {
|
||||
name: inpName ? inpName.value : '',
|
||||
api_name: inpApiName ? inpApiName.value : ''
|
||||
api_name: inpApiName ? inpApiName.value : '',
|
||||
section_id: section.id,
|
||||
});
|
||||
toast(res && res.ok ? 'Template gespeichert' : 'Speichern fehlgeschlagen', !!(res && res.ok));
|
||||
dlg && dlg.close();
|
||||
cleanup();
|
||||
loadList('templates');
|
||||
}catch(e){
|
||||
if (typeof window.loadList === 'function') window.loadList(section);
|
||||
} catch {
|
||||
toast('Speichern fehlgeschlagen', false);
|
||||
}
|
||||
}
|
||||
@@ -106,19 +102,31 @@ async function openTemplateEditor(id){
|
||||
dlg && dlg.showModal();
|
||||
}
|
||||
|
||||
export async function loadList(resource){
|
||||
const el=document.getElementById(`view-${resource}`); if(!el) return;
|
||||
export async function loadList(section) {
|
||||
const el = document.getElementById('view-content');
|
||||
if (typeof section === 'string') {
|
||||
const sections = window.__sectionsConfig || [];
|
||||
section = sections.find(s => String(s.slug || '').toLowerCase() === section.toLowerCase())
|
||||
|| sections.find(s => String(s.name || '').toLowerCase() === section.toLowerCase())
|
||||
|| null;
|
||||
} else if (typeof section === 'number') {
|
||||
const sections = window.__sectionsConfig || [];
|
||||
section = sections.find(s => Number(s.id) === section) || null;
|
||||
}
|
||||
section = section || window.__activeSection || null;
|
||||
if (!el || !section) return;
|
||||
|
||||
const label = resource.charAt(0).toUpperCase()+resource.slice(1);
|
||||
const label = section.name || 'Section';
|
||||
const isTemplate = Number(section.is_template) === 1;
|
||||
el.innerHTML = `<div class='rounded-2xl border bg-white overflow-hidden'>
|
||||
<div class='px-4 py-2 border-b bg-gray-50 text-sm font-medium flex items-center gap-3'>
|
||||
<span>${label}</span>
|
||||
<span>${esc(label)}</span>
|
||||
<div class='ms-auto flex items-center gap-2'>
|
||||
<div class='flex items-center gap-1'>
|
||||
<input id='filter-${resource}' class='input text-sm' placeholder='Suche Name/API' />
|
||||
<button id='filter-${resource}-reset' class='btn' type='button' title='Suche zurücksetzen'>×</button>
|
||||
<input id='filter-section' class='input text-sm' placeholder='Suche Name${isTemplate ? '/API' : ''}' />
|
||||
<button id='filter-section-reset' class='btn' type='button' title='Suche zurücksetzen'>×</button>
|
||||
</div>
|
||||
<select id='sort-${resource}' class='input text-sm'>
|
||||
<select id='sort-section' class='input text-sm'>
|
||||
<option value='created_asc'>Erstelldatum (aufsteigend)</option>
|
||||
<option value='name_asc'>Name A–Z</option>
|
||||
<option value='name_desc'>Name Z–A</option>
|
||||
@@ -126,13 +134,19 @@ export async function loadList(resource){
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div id='list-${resource}' class='divide-y'>Lade …</div></div>`;
|
||||
<div id='list-section' class='divide-y'>Lade …</div></div>`;
|
||||
|
||||
const data=await apiList(resource);
|
||||
const list=el.querySelector(`#list-${resource}`);
|
||||
const filterInput=el.querySelector(`#filter-${resource}`);
|
||||
const filterReset=el.querySelector(`#filter-${resource}-reset`);
|
||||
const sortSelect=el.querySelector(`#sort-${resource}`);
|
||||
let data = [];
|
||||
try {
|
||||
data = await fetchContentList(section.id);
|
||||
} catch (err) {
|
||||
list.innerHTML = `<div class='p-4 text-sm text-gray-500'>${esc(err.message || 'Laden fehlgeschlagen')}</div>`;
|
||||
return;
|
||||
}
|
||||
const list = el.querySelector('#list-section');
|
||||
const filterInput = el.querySelector('#filter-section');
|
||||
const filterReset = el.querySelector('#filter-section-reset');
|
||||
const sortSelect = el.querySelector('#sort-section');
|
||||
|
||||
if (!Array.isArray(data) || data.length === 0) {
|
||||
list.innerHTML = `<div class='p-4 text-sm text-gray-500'>Keine Einträge</div>`;
|
||||
@@ -171,50 +185,33 @@ export async function loadList(resource){
|
||||
if (!query) return true;
|
||||
const q = query.toLowerCase();
|
||||
const name = String(item?.name || '').toLowerCase();
|
||||
const api = resource === 'templates' ? String(item?.api_name || '').toLowerCase() : '';
|
||||
const api = isTemplate ? String(item?.api_name || '').toLowerCase() : '';
|
||||
return name.includes(q) || (api && api.includes(q));
|
||||
}
|
||||
|
||||
function parentBadge(r,it){
|
||||
if(r==='sections'&&it.template_id) return `<span class="chip"><span class="dot"></span> Template #${it.template_id}${it.template_name ? ' · '+esc(it.template_name) : ''}</span>`;
|
||||
if(r==='blocks'&&it.section_id) return `<span class="chip"><span class="dot"></span> Section #${it.section_id}${it.section_name ? ' · '+esc(it.section_name) : ''}</span>`;
|
||||
if(r==='snippets'&&it.block_id) return `<span class="chip"><span class="dot"></span> Block #${it.block_id}${it.block_name ? ' · '+esc(it.block_name) : ''}</span>`;
|
||||
return '<span class="chip"><span class="dot"></span> frei</span>';
|
||||
}
|
||||
|
||||
function render(items) {
|
||||
list.innerHTML = items.map(item => {
|
||||
const name = esc(item.name || '');
|
||||
const apiName = resource==='templates' ? esc(item.api_name||'') : '';
|
||||
const apiLine = (resource==='templates' && apiName) ? `<div class='text-xs text-slate-500'>API: ${apiName}</div>` : '';
|
||||
const apiName = isTemplate ? esc(item.api_name || '') : '';
|
||||
const apiLine = (isTemplate && apiName) ? `<div class='text-xs text-slate-500'>API: ${apiName}</div>` : '';
|
||||
const nameCell = `<div class='min-w-48'>
|
||||
<div class='font-medium truncate' title="${name}">${name || '(ohne Name)'}</div>
|
||||
${apiLine}
|
||||
</div>`;
|
||||
const openBtn = (['templates','sections','blocks'].includes(resource))
|
||||
? `<button class='btn' data-open='${resource}:${item.id}'>Im E-Mail-Editor öffnen</button>` : '';
|
||||
|
||||
const editBtn = (resource==='snippets')
|
||||
? `<button class='btn' data-edit='snippets:${item.id}'>Im Editor</button>` : '';
|
||||
|
||||
const editTplBtn = (resource==='templates')
|
||||
? `<button class='btn' data-edit='templates:${item.id}'>Bearbeiten</button>` : '';
|
||||
|
||||
const testBtn = resource==='templates'
|
||||
? `<button class='btn' data-test='${item.id}' data-name='${name}'>Testversand</button>` : '';
|
||||
|
||||
const prevBtn = `<button class='btn' data-preview='${resource}:${item.id}'>Vorschau</button>`;
|
||||
const delBtn = `<button class='btn btn-danger' data-del='${resource}:${item.id}' data-name='${name}'>Löschen</button>`;
|
||||
const openBtn = `<button class='btn' data-open='${item.id}'>Im Editor öffnen</button>`;
|
||||
const editTplBtn = isTemplate ? `<button class='btn' data-edit='${item.id}'>Bearbeiten</button>` : '';
|
||||
const testBtn = isTemplate ? `<button class='btn' data-test='${item.id}' data-name='${name}'>Testversand</button>` : '';
|
||||
const prevBtn = `<button class='btn' data-preview='${item.id}'>Vorschau</button>`;
|
||||
const delBtn = `<button class='btn btn-danger' data-del='${item.id}' data-name='${name}'>Löschen</button>`;
|
||||
|
||||
return `<div class='p-3 flex items-center gap-3'>
|
||||
${nameCell}
|
||||
<div class='text-xs text-gray-500'>#${item.id}</div>
|
||||
<div class='text-xs'>${parentBadge(resource,item)}</div>
|
||||
<div class='ms-auto flex gap-2'>${[openBtn, editBtn, editTplBtn, testBtn, prevBtn, delBtn].filter(Boolean).join('')}</div>
|
||||
<div class='ms-auto flex gap-2'>${[openBtn, editTplBtn, testBtn, prevBtn, delBtn].filter(Boolean).join('')}</div>
|
||||
</div>`;
|
||||
}).join('');
|
||||
|
||||
bindListHandlers(list, resource);
|
||||
bindListHandlers(list);
|
||||
}
|
||||
|
||||
function applyFilter() {
|
||||
@@ -232,9 +229,7 @@ export async function loadList(resource){
|
||||
} catch {}
|
||||
}
|
||||
|
||||
if (filterInput) {
|
||||
filterInput.addEventListener('input', applyFilter);
|
||||
}
|
||||
if (filterInput) filterInput.addEventListener('input', applyFilter);
|
||||
if (filterReset) {
|
||||
filterReset.addEventListener('click', () => {
|
||||
if (filterInput) {
|
||||
@@ -254,61 +249,33 @@ export async function loadList(resource){
|
||||
|
||||
applyFilter();
|
||||
|
||||
const delDlg=document.getElementById('deleteDialog'),
|
||||
delText=document.getElementById('deleteText'),
|
||||
delForm=document.getElementById('deleteForm'),
|
||||
delCancel=document.getElementById('deleteCancel');
|
||||
|
||||
let pending=null;
|
||||
delCancel && (delCancel.onclick=()=>{pending=null;delDlg.close();});
|
||||
|
||||
// --- Editor öffnen (ANPASSUNG) -----------------------------------------
|
||||
function bindListHandlers(scope, resName){
|
||||
scope.querySelectorAll('[data-open]').forEach(b=>b.addEventListener('click', async ()=>{
|
||||
const [res,id]=b.dataset.open.split(':');
|
||||
|
||||
// Detail laden, um Name + aktuellen HTML/Content zu haben
|
||||
const obj = await apiGet(res,id);
|
||||
const name = obj?.name || '';
|
||||
const html = obj ? (obj.html ?? obj.content ?? '') : '';
|
||||
|
||||
// Globale Kontexte (werden von Editor/anderen Modulen genutzt)
|
||||
window.__currentItemId = Number(id);
|
||||
window.__currentEditorCtx = { id:Number(id), mode:res };
|
||||
|
||||
// Bevorzugt EditorUI.open nutzen; Fallback: __openEditor (Bestand)
|
||||
if (window.EditorUI && typeof window.EditorUI.open === 'function') {
|
||||
window.EditorUI.open({ id:Number(id), name, html }, res);
|
||||
} else if (window.__openEditor) {
|
||||
window.__openEditor({ resource:res, id:Number(id), name, html });
|
||||
} else {
|
||||
console.warn('Kein Editor-Entry-Point gefunden (EditorUI.open / __openEditor).');
|
||||
toast('Editor ist nicht initialisiert.', false);
|
||||
}
|
||||
}));
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
// edit snippet
|
||||
scope.querySelectorAll('[data-edit]').forEach(b=>b.addEventListener('click', async ()=>{
|
||||
const [res, id] = b.dataset.edit.split(':');
|
||||
if (res === 'snippets') await openSnippetEditor(id);
|
||||
if (res === 'templates') await openTemplateEditor(id);
|
||||
function bindListHandlers(scope) {
|
||||
scope.querySelectorAll('[data-open]').forEach(btn => btn.addEventListener('click', () => {
|
||||
const id = Number(btn.dataset.open || 0);
|
||||
const item = data.find(it => Number(it.id) === id);
|
||||
if (!item) return;
|
||||
openContentEditor(item, section);
|
||||
}));
|
||||
|
||||
// preview
|
||||
const prevDlg=document.getElementById('previewDialog'), prevFrame=document.getElementById('previewFrame');
|
||||
scope.querySelectorAll('[data-preview]').forEach(b=>b.addEventListener('click', async ()=>{
|
||||
const [res,id]=b.dataset.preview.split(':');
|
||||
const obj=await apiGet(res,id);
|
||||
const html=(obj?.html||obj?.content||'<em>(leer)</em)');
|
||||
scope.querySelectorAll('[data-edit]').forEach(btn => btn.addEventListener('click', () => {
|
||||
const id = Number(btn.dataset.edit || 0);
|
||||
const item = data.find(it => Number(it.id) === id);
|
||||
if (item) openTemplateEditor(item, section);
|
||||
}));
|
||||
|
||||
const prevDlg = document.getElementById('previewDialog');
|
||||
const prevFrame = document.getElementById('previewFrame');
|
||||
scope.querySelectorAll('[data-preview]').forEach(btn => btn.addEventListener('click', async () => {
|
||||
const id = Number(btn.dataset.preview || 0);
|
||||
const obj = await fetchContentItem(id, section.id);
|
||||
const html = (obj?.html || obj?.content || '<em>(leer)</em>');
|
||||
prevFrame.srcdoc = '<!doctype html><html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"></head><body>' + html + '</body></html>';
|
||||
prevDlg.showModal();
|
||||
}));
|
||||
|
||||
// test send (templates only)
|
||||
scope.querySelectorAll('[data-test]').forEach(b=>b.addEventListener('click', ()=>{
|
||||
const id = Number(b.dataset.test || '0');
|
||||
const nm = b.dataset.name || '';
|
||||
scope.querySelectorAll('[data-test]').forEach(btn => btn.addEventListener('click', () => {
|
||||
const id = Number(btn.dataset.test || 0);
|
||||
const nm = btn.dataset.name || '';
|
||||
if (!id) {
|
||||
toast('Testversand: Ungültige ID', false);
|
||||
return;
|
||||
@@ -320,34 +287,43 @@ export async function loadList(resource){
|
||||
}
|
||||
}));
|
||||
|
||||
// delete
|
||||
scope.querySelectorAll('[data-del]').forEach(b=>b.addEventListener('click', async ()=>{
|
||||
const [res,id]=b.dataset.del.split(':'); const nm=b.dataset.name||'';
|
||||
let usage = null;
|
||||
try {
|
||||
const detail = await apiGet(res, id);
|
||||
usage = detail?.usage || null;
|
||||
} catch {}
|
||||
pending={res,id,nm,usage};
|
||||
const usageWarn = formatUsage(usage);
|
||||
delText && (delText.innerHTML=`Soll <strong>${nm || '(ohne Name)'} #${id}</strong> aus <strong>${res}</strong> wirklich gelöscht werden?<br><span class="text-rose-600">Achtung:</span> Kinder-Elemente werden <em>nicht</em> automatisch mit gelöscht.${usageWarn}`);
|
||||
const delDlg = document.getElementById('deleteDialog');
|
||||
const delText = document.getElementById('deleteText');
|
||||
const delForm = document.getElementById('deleteForm');
|
||||
const delCancel = document.getElementById('deleteCancel');
|
||||
let pending = null;
|
||||
|
||||
delCancel && (delCancel.onclick = () => { pending = null; delDlg.close(); });
|
||||
|
||||
scope.querySelectorAll('[data-del]').forEach(btn => btn.addEventListener('click', () => {
|
||||
const id = Number(btn.dataset.del || 0);
|
||||
const nm = btn.dataset.name || '';
|
||||
pending = { id, name: nm };
|
||||
if (delText) {
|
||||
delText.innerHTML = `Soll <strong>${nm || '(ohne Name)'} #${id}</strong> wirklich gelöscht werden?`;
|
||||
}
|
||||
delDlg.showModal();
|
||||
}));
|
||||
}
|
||||
delForm && (delForm.onsubmit=async(e)=>{
|
||||
e.preventDefault();
|
||||
|
||||
delForm && (delForm.onsubmit = async (ev) => {
|
||||
ev.preventDefault();
|
||||
if (!pending) return delDlg.close();
|
||||
const r=await apiDelete(pending.res,pending.id);
|
||||
const res = await apiAction('content.delete', { method: 'POST', data: { id: pending.id, section_id: section.id } });
|
||||
delDlg.close();
|
||||
toast(r&&r.ok?'Gelöscht':'Löschen fehlgeschlagen', !!(r&&r.ok), {duration:3000});
|
||||
loadList(resource);
|
||||
toast(res && res.ok ? 'Gelöscht' : 'Löschen fehlgeschlagen', !!(res && res.ok), { duration: 3000 });
|
||||
loadList(section);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export function initLists() {
|
||||
loadList('templates');
|
||||
// Public reload helper (wird vom Snippet-Editor genutzt)
|
||||
if (window.__sectionsReady && typeof window.__sectionsReady.then === 'function') {
|
||||
window.__sectionsReady.then(() => {
|
||||
if (window.__activeSection) loadList(window.__activeSection);
|
||||
});
|
||||
} else if (window.__activeSection) {
|
||||
loadList(window.__activeSection);
|
||||
}
|
||||
window.__reloadList = loadList;
|
||||
// Backwards compat (falls woanders genutzt)
|
||||
window.loadList = loadList;
|
||||
}
|
||||
|
||||
@@ -1,11 +1,60 @@
|
||||
export function initTabs(){
|
||||
const tabs=document.querySelectorAll('nav [data-tab]'); if(!tabs.length) return;
|
||||
const views={ templates:document.getElementById('view-templates'), sections:document.getElementById('view-sections'), blocks:document.getElementById('view-blocks'), snippets:document.getElementById('view-snippets') };
|
||||
tabs.forEach(btn=>btn.addEventListener('click',()=>{
|
||||
tabs.forEach(b=>b.classList.remove('bg-sky-50','text-sky-700'));
|
||||
import { apiAction, toast } from './api.js';
|
||||
|
||||
function renderTabs(nav, sections, activeId) {
|
||||
nav.innerHTML = '';
|
||||
sections.forEach((section, idx) => {
|
||||
const btn = document.createElement('button');
|
||||
btn.type = 'button';
|
||||
btn.dataset.sectionId = String(section.id);
|
||||
btn.className = 'px-4 py-2 text-sm border-e';
|
||||
btn.textContent = section.name || `Section ${idx + 1}`;
|
||||
if (section.id === activeId) {
|
||||
btn.classList.add('bg-sky-50', 'text-sky-700');
|
||||
document.querySelectorAll('.view').forEach(v=>v.classList.add('hidden'));
|
||||
const tab=btn.dataset.tab; views[tab]?.classList.remove('hidden');
|
||||
window.loadList && window.loadList(tab);
|
||||
}));
|
||||
}
|
||||
nav.appendChild(btn);
|
||||
});
|
||||
}
|
||||
|
||||
function pickDefaultSection(sections) {
|
||||
if (!Array.isArray(sections) || !sections.length) return null;
|
||||
const tpl = sections.find(s => Number(s.is_template) === 1);
|
||||
return tpl || sections[0];
|
||||
}
|
||||
|
||||
export async function initTabs() {
|
||||
const nav = document.getElementById('sectionTabs');
|
||||
if (!nav) return;
|
||||
const readyPromise = (async () => {
|
||||
try {
|
||||
const res = await apiAction('sections_config.list', { method: 'GET' });
|
||||
const sections = Array.isArray(res?.items) ? res.items : [];
|
||||
if (!sections.length) {
|
||||
nav.innerHTML = '<span class="px-4 py-2 text-sm text-slate-500">Keine Sections</span>';
|
||||
return;
|
||||
}
|
||||
window.__sectionsConfig = sections;
|
||||
const active = pickDefaultSection(sections);
|
||||
window.__activeSection = active;
|
||||
renderTabs(nav, sections, active?.id);
|
||||
nav.querySelectorAll('button[data-section-id]').forEach(btn => {
|
||||
btn.addEventListener('click', () => {
|
||||
const id = Number(btn.dataset.sectionId || 0);
|
||||
const next = sections.find(s => Number(s.id) === id);
|
||||
if (!next) return;
|
||||
window.__activeSection = next;
|
||||
renderTabs(nav, sections, next.id);
|
||||
if (typeof window.loadList === 'function') {
|
||||
window.loadList(next);
|
||||
}
|
||||
});
|
||||
});
|
||||
if (typeof window.loadList === 'function' && active) {
|
||||
window.loadList(active);
|
||||
}
|
||||
} catch (err) {
|
||||
toast(err.message || 'Sections konnten nicht geladen werden', false);
|
||||
}
|
||||
})();
|
||||
window.__sectionsReady = readyPromise;
|
||||
return readyPromise;
|
||||
}
|
||||
@@ -25,6 +25,15 @@ let teamTable;
|
||||
let userForm;
|
||||
let senderTable;
|
||||
let senderForm;
|
||||
let sectionsList;
|
||||
let sectionsCreateForm;
|
||||
let sectionNameInput;
|
||||
let sectionsDeleteDialog;
|
||||
let sectionsDeleteForm;
|
||||
let sectionsDeleteTarget;
|
||||
let sectionsDeleteText;
|
||||
let sectionsDeleteCancel;
|
||||
let sectionDragId = null;
|
||||
let menuInitialized = false;
|
||||
let menuOpen = false;
|
||||
let debugButton;
|
||||
@@ -75,6 +84,14 @@ export function initAccountPage() {
|
||||
adminTablesAddBtn = document.getElementById('adminBridgeTablesAdd');
|
||||
adminTablesRemoveBtn = document.getElementById('adminBridgeTablesRemove');
|
||||
adminLoadBridgeBtn = document.getElementById('btn-admin-load-bridge');
|
||||
sectionsList = document.getElementById('sectionsList');
|
||||
sectionsCreateForm = document.getElementById('sectionsCreateForm');
|
||||
sectionNameInput = document.getElementById('sectionNameInput');
|
||||
sectionsDeleteDialog = document.getElementById('sectionsDeleteDialog');
|
||||
sectionsDeleteForm = document.getElementById('sectionsDeleteForm');
|
||||
sectionsDeleteTarget = document.getElementById('sectionsDeleteTarget');
|
||||
sectionsDeleteText = document.getElementById('sectionsDeleteText');
|
||||
sectionsDeleteCancel = document.getElementById('sectionsDeleteCancel');
|
||||
|
||||
document.getElementById('btn-user-add')?.addEventListener('click', () => openUserForm());
|
||||
document.getElementById('userFormCancel')?.addEventListener('click', () => closeUserForm());
|
||||
@@ -122,6 +139,8 @@ export function initAccountPage() {
|
||||
refreshBridgeTablesFromEndpoint();
|
||||
});
|
||||
|
||||
initSectionsManager();
|
||||
|
||||
window.addEventListener('bridge-setup-updated', (ev) => {
|
||||
const setup = ev?.detail || {};
|
||||
refreshAdminTables(setup.tables || [], state.settings.bridge_tables || []);
|
||||
@@ -132,6 +151,151 @@ export function initAccountPage() {
|
||||
updateRoleVisibility();
|
||||
}
|
||||
|
||||
function initSectionsManager() {
|
||||
if (!sectionsList || !sectionsCreateForm || !sectionNameInput) return;
|
||||
|
||||
sectionsCreateForm.addEventListener('submit', async (ev) => {
|
||||
ev.preventDefault();
|
||||
const name = sectionNameInput.value.trim();
|
||||
if (!name) return;
|
||||
try {
|
||||
const res = await apiAction('sections_config.create', { method: 'POST', data: { name } });
|
||||
if (!res?.ok) throw new Error(res?.error || 'Erstellen fehlgeschlagen');
|
||||
sectionNameInput.value = '';
|
||||
await loadSectionsConfig();
|
||||
toast('Section erstellt', true);
|
||||
} catch (err) {
|
||||
toast(err.message || 'Erstellen fehlgeschlagen', false);
|
||||
}
|
||||
});
|
||||
|
||||
sectionsDeleteCancel && (sectionsDeleteCancel.onclick = () => {
|
||||
sectionsDeleteDialog?.close();
|
||||
});
|
||||
|
||||
sectionsDeleteForm?.addEventListener('submit', async (ev) => {
|
||||
ev.preventDefault();
|
||||
const id = Number(sectionsDeleteForm?.dataset?.sectionId || 0);
|
||||
const target = Number(sectionsDeleteTarget?.value || 0);
|
||||
if (!id || !target) return;
|
||||
try {
|
||||
const res = await apiAction('sections_config.delete', { method: 'POST', data: { id, move_to: target } });
|
||||
if (!res?.ok) throw new Error(res?.error || 'Löschen fehlgeschlagen');
|
||||
sectionsDeleteDialog?.close();
|
||||
await loadSectionsConfig();
|
||||
toast('Section gelöscht', true);
|
||||
} catch (err) {
|
||||
toast(err.message || 'Löschen fehlgeschlagen', false);
|
||||
}
|
||||
});
|
||||
|
||||
loadSectionsConfig();
|
||||
}
|
||||
|
||||
async function loadSectionsConfig() {
|
||||
try {
|
||||
const res = await apiAction('sections_config.list', { method: 'GET' });
|
||||
const items = Array.isArray(res?.items) ? res.items : [];
|
||||
renderSectionsList(items);
|
||||
} catch (err) {
|
||||
toast(err.message || 'Sections konnten nicht geladen werden', false);
|
||||
}
|
||||
}
|
||||
|
||||
function renderSectionsList(items) {
|
||||
if (!sectionsList) return;
|
||||
const rows = items || [];
|
||||
sectionsList.innerHTML = rows.map((item) => {
|
||||
const isTemplate = Number(item.is_template) === 1;
|
||||
const dragAttr = isTemplate ? '' : 'draggable="true"';
|
||||
const badge = isTemplate ? '<span class="text-xs text-sky-700 bg-sky-100 px-2 py-0.5 rounded-full">Fix</span>' : '';
|
||||
const editBtn = isTemplate ? '' : `<button type="button" class="btn text-xs" data-edit="${item.id}">Umbenennen</button>`;
|
||||
const delBtn = isTemplate ? '' : `<button type="button" class="btn btn-danger text-xs" data-del="${item.id}">Löschen</button>`;
|
||||
return `<li class="section-item flex items-center gap-3 border rounded-lg px-3 py-2 bg-white" data-id="${item.id}" ${dragAttr}>
|
||||
<span class="cursor-grab text-slate-400 select-none">☰</span>
|
||||
<div class="flex-1">
|
||||
<div class="font-medium">${escapeHtml(item.name || '')}</div>
|
||||
<div class="text-xs text-slate-500">${escapeHtml(item.slug || '')}</div>
|
||||
</div>
|
||||
${badge}
|
||||
<div class="flex gap-2">${editBtn}${delBtn}</div>
|
||||
</li>`;
|
||||
}).join('');
|
||||
|
||||
sectionsList.querySelectorAll('[data-edit]').forEach(btn => btn.addEventListener('click', async () => {
|
||||
const id = Number(btn.dataset.edit || 0);
|
||||
const current = rows.find(r => Number(r.id) === id);
|
||||
if (!current) return;
|
||||
const next = prompt('Neuer Name', current.name || '');
|
||||
if (!next || next.trim() === current.name) return;
|
||||
try {
|
||||
const res = await apiAction('sections_config.update', { method: 'POST', data: { id, name: next.trim() } });
|
||||
if (!res?.ok) throw new Error(res?.error || 'Speichern fehlgeschlagen');
|
||||
await loadSectionsConfig();
|
||||
toast('Section gespeichert', true);
|
||||
} catch (err) {
|
||||
toast(err.message || 'Speichern fehlgeschlagen', false);
|
||||
}
|
||||
}));
|
||||
|
||||
sectionsList.querySelectorAll('[data-del]').forEach(btn => btn.addEventListener('click', () => {
|
||||
const id = Number(btn.dataset.del || 0);
|
||||
const current = rows.find(r => Number(r.id) === id);
|
||||
if (!current) return;
|
||||
const targets = rows.filter(r => Number(r.id) !== id);
|
||||
if (!targets.length) {
|
||||
toast('Keine Ziel-Section verfügbar', false);
|
||||
return;
|
||||
}
|
||||
if (sectionsDeleteText) {
|
||||
sectionsDeleteText.textContent = `Section "${current.name}" löschen?`;
|
||||
}
|
||||
if (sectionsDeleteTarget) {
|
||||
sectionsDeleteTarget.innerHTML = targets
|
||||
.map(r => `<option value="${r.id}">${escapeHtml(r.name || '')}</option>`)
|
||||
.join('');
|
||||
}
|
||||
if (sectionsDeleteForm) {
|
||||
sectionsDeleteForm.dataset.sectionId = String(id);
|
||||
}
|
||||
sectionsDeleteDialog?.showModal?.();
|
||||
}));
|
||||
|
||||
sectionsList.querySelectorAll('[draggable="true"]').forEach(item => {
|
||||
item.addEventListener('dragstart', (ev) => {
|
||||
sectionDragId = item.dataset.id || null;
|
||||
ev.dataTransfer?.setData('text/plain', sectionDragId || '');
|
||||
});
|
||||
item.addEventListener('dragend', () => {
|
||||
sectionDragId = null;
|
||||
});
|
||||
item.addEventListener('dragover', (ev) => {
|
||||
ev.preventDefault();
|
||||
});
|
||||
item.addEventListener('drop', async (ev) => {
|
||||
ev.preventDefault();
|
||||
const targetId = item.dataset.id || null;
|
||||
if (!sectionDragId || !targetId || sectionDragId === targetId) return;
|
||||
const ids = Array.from(sectionsList.querySelectorAll('[data-id]')).map(el => el.getAttribute('data-id'));
|
||||
const fromIndex = ids.indexOf(sectionDragId);
|
||||
const toIndex = ids.indexOf(targetId);
|
||||
if (fromIndex === -1 || toIndex === -1) return;
|
||||
ids.splice(fromIndex, 1);
|
||||
ids.splice(toIndex, 0, sectionDragId);
|
||||
try {
|
||||
const res = await apiAction('sections_config.reorder', { method: 'POST', data: { order: ids } });
|
||||
if (!res?.ok) throw new Error(res?.error || 'Sortierung fehlgeschlagen');
|
||||
await loadSectionsConfig();
|
||||
toast('Sortierung gespeichert', true);
|
||||
} catch (err) {
|
||||
toast(err.message || 'Sortierung fehlgeschlagen', false);
|
||||
} finally {
|
||||
sectionDragId = null;
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function isOwner() {
|
||||
return (window.__currentUser?.role || '').toLowerCase() === 'owner';
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<?php
|
||||
$mode = strtolower($_GET['mode'] ?? 'templates');
|
||||
$id = (int)($_GET['id'] ?? 0);
|
||||
$sectionId = (int)($_GET['section_id'] ?? 0);
|
||||
$assetVersion = defined('ASSET_VERSION') ? ASSET_VERSION : time();
|
||||
$scheme = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') ? 'https' : 'http';
|
||||
$host = $_SERVER['HTTP_HOST'] ?? 'localhost';
|
||||
@@ -48,6 +49,7 @@ if ($fontSources) {
|
||||
<script>
|
||||
window.__editorMode = "<?=htmlspecialchars($mode, ENT_QUOTES)?>";
|
||||
window.__editorId = <?= $id ?>;
|
||||
window.__editorSectionId = <?= $sectionId ?>;
|
||||
window.BridgeParts = window.BridgeParts || {};
|
||||
window.BridgeParts.ENABLE_EDITOR_EXTENSIONS = true;
|
||||
window.BridgeParts.ENABLE_EDITOR_BEHAVIOR = false;
|
||||
@@ -57,7 +59,9 @@ if ($fontSources) {
|
||||
window.BridgeParts.LOG_CONFIG = window.BridgeParts.LOG_CONFIG || {};
|
||||
window.BridgeParts.LOG_CONFIG.INFO_ENABLED = false;
|
||||
window.BridgeParts.LOG_CONFIG.DATA_ENABLED = false;
|
||||
window.BridgeParts.USE_DYNAMIC_SECTIONS = true;
|
||||
window.BridgeParts.CURRENT_ENTITY_ID = window.BridgeParts.CURRENT_ENTITY_ID || <?= $id ?>;
|
||||
window.BridgeParts.CURRENT_SECTION_ID = window.BridgeParts.CURRENT_SECTION_ID || <?= $sectionId ?>;
|
||||
window.BridgeParts.API_KERNEL_URL = window.BridgeParts.API_KERNEL_URL || '/api.php';
|
||||
window.BridgeParts.API_BASE = window.BridgeParts.API_BASE || window.BridgeParts.API_KERNEL_URL;
|
||||
window.BridgeParts.STORAGE_URL_BASE = window.BridgeParts.STORAGE_URL_BASE || window.BridgeParts.API_BASE;
|
||||
|
||||
@@ -7,12 +7,7 @@ $navLinks = [];
|
||||
|
||||
ob_start();
|
||||
?>
|
||||
<nav class="isolate inline-flex flex-wrap rounded-2xl shadow-sm border bg-white overflow-hidden">
|
||||
<button type="button" data-tab="templates" class="px-4 py-2 text-sm border-e bg-sky-50 text-sky-700">Templates</button>
|
||||
<button type="button" data-tab="sections" class="px-4 py-2 text-sm border-e">Sections</button>
|
||||
<button type="button" data-tab="blocks" class="px-4 py-2 text-sm border-e">Blocks</button>
|
||||
<button type="button" data-tab="snippets" class="px-4 py-2 text-sm">Snippets</button>
|
||||
</nav>
|
||||
<nav id="sectionTabs" class="isolate inline-flex flex-wrap rounded-2xl shadow-sm border bg-white overflow-hidden"></nav>
|
||||
<button id="btn-new" type="button" class="btn">Neu …</button>
|
||||
<?php
|
||||
$headerTabs = ob_get_clean();
|
||||
@@ -25,10 +20,7 @@ require __DIR__ . '/../partials/structure/layout_start.php';
|
||||
<p class="text-sm text-slate-500">Wähle eine Kategorie, um Templates, Sections, Blocks oder Snippets zu pflegen.</p>
|
||||
</header>
|
||||
|
||||
<section id="view-templates" class="view"></section>
|
||||
<section id="view-sections" class="view hidden"></section>
|
||||
<section id="view-blocks" class="view hidden"></section>
|
||||
<section id="view-snippets" class="view hidden"></section>
|
||||
<section id="view-content" class="view"></section>
|
||||
</main>
|
||||
|
||||
<!-- Create Dialog -->
|
||||
|
||||
42
schema.sql
42
schema.sql
@@ -272,5 +272,47 @@ CREATE TABLE IF NOT EXISTS `emailtemplate_template_usage` (
|
||||
CONSTRAINT `fk_usage_template` FOREIGN KEY (`template_id`) REFERENCES `emailtemplate_templates` (`id`) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
|
||||
|
||||
-- Tabelle: emailtemplate_content_sections
|
||||
CREATE TABLE IF NOT EXISTS `emailtemplate_content_sections` (
|
||||
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
|
||||
`customer_id` int(10) unsigned NOT NULL,
|
||||
`name` varchar(255) NOT NULL,
|
||||
`slug` varchar(190) NOT NULL,
|
||||
`position` int(11) NOT NULL DEFAULT 0,
|
||||
`is_template` tinyint(1) NOT NULL DEFAULT 0,
|
||||
`created_at` timestamp NOT NULL DEFAULT current_timestamp(),
|
||||
`updated_at` timestamp NOT NULL DEFAULT current_timestamp() ON UPDATE current_timestamp(),
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uq_sections_customer_slug` (`customer_id`,`slug`),
|
||||
UNIQUE KEY `uq_sections_customer_name` (`customer_id`,`name`),
|
||||
KEY `idx_sections_customer` (`customer_id`),
|
||||
KEY `idx_sections_sort` (`customer_id`,`position`,`id`),
|
||||
CONSTRAINT `fk_content_sections_customer` FOREIGN KEY (`customer_id`) REFERENCES `emailtemplate_customers` (`id`) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
|
||||
|
||||
-- Tabelle: emailtemplate_content_items
|
||||
CREATE TABLE IF NOT EXISTS `emailtemplate_content_items` (
|
||||
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
|
||||
`customer_id` int(10) unsigned NOT NULL,
|
||||
`section_id` int(10) unsigned NOT NULL,
|
||||
`name` varchar(255) NOT NULL,
|
||||
`api_name` varchar(190) DEFAULT NULL,
|
||||
`category` varchar(100) DEFAULT NULL,
|
||||
`json_content` mediumtext DEFAULT NULL,
|
||||
`html` mediumtext DEFAULT NULL,
|
||||
`editor_type` varchar(32) DEFAULT NULL,
|
||||
`craft_json` mediumtext DEFAULT NULL,
|
||||
`settings_json` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL CHECK (json_valid(`settings_json`)),
|
||||
`created_at` timestamp NOT NULL DEFAULT current_timestamp(),
|
||||
`updated_at` timestamp NOT NULL DEFAULT current_timestamp() ON UPDATE current_timestamp(),
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uq_items_api` (`customer_id`,`api_name`),
|
||||
KEY `idx_items_customer` (`customer_id`),
|
||||
KEY `idx_items_section` (`section_id`),
|
||||
KEY `idx_items_name` (`name`),
|
||||
CONSTRAINT `fk_content_items_section` FOREIGN KEY (`section_id`) REFERENCES `emailtemplate_content_sections` (`id`) ON DELETE CASCADE ON UPDATE CASCADE,
|
||||
CONSTRAINT `fk_content_items_customer` FOREIGN KEY (`customer_id`) REFERENCES `emailtemplate_customers` (`id`) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
|
||||
|
||||
SET FOREIGN_KEY_CHECKS = 1;
|
||||
-- Ende des Schema-Dumps
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user