diff --git a/public/assets/js/bridge/blocks-api.js b/public/assets/js/bridge/blocks-api.js index 41c6bf6..6c66495 100644 --- a/public/assets/js/bridge/blocks-api.js +++ b/public/assets/js/bridge/blocks-api.js @@ -502,6 +502,12 @@ // 🚨 KRITISCH: Server erwartet das Feld 'json' json: jsonProjectDataRaw, }; + const activateNext = B.NEXT_ACTIVATE_VERSION || window.NEXT_ACTIVATE_VERSION; + if (activateNext) { + dataToSend.activate_version = 1; + B.NEXT_ACTIVATE_VERSION = 0; + window.NEXT_ACTIVATE_VERSION = 0; + } if (SECTION_ID) { dataToSend.section_id = SECTION_ID; } diff --git a/public/assets/js/bridge/library-api.js b/public/assets/js/bridge/library-api.js index 4353fca..345b2b0 100644 --- a/public/assets/js/bridge/library-api.js +++ b/public/assets/js/bridge/library-api.js @@ -191,7 +191,7 @@ return []; } const promises = sections.map(section => - fetchData('content', 'list', { section_id: section.id }) + fetchData('content', 'list', { section_id: section.id, active_only: 1 }) .then(items => (Array.isArray(items) ? items : []).map(i => ({ ...i, kind: String(section.slug || '').toLowerCase(), diff --git a/public/assets/js/ui-editor.js b/public/assets/js/ui-editor.js index 7650fa9..fe4ba2b 100644 --- a/public/assets/js/ui-editor.js +++ b/public/assets/js/ui-editor.js @@ -33,6 +33,11 @@ export function initEditor() { const btnUnsavedCancel = document.getElementById('btn-unsaved-cancel'); const btnUnsavedDiscard = document.getElementById('btn-unsaved-discard'); const btnUnsavedSave = document.getElementById('btn-unsaved-save'); + const btnDeactivateVersion = document.getElementById('btn-deactivate-version'); + const activateDialog = document.getElementById('activateVersionDialog'); + const btnActivateCancel = document.getElementById('btn-activate-cancel'); + const btnActivateNo = document.getElementById('btn-activate-no'); + const btnActivateYes = document.getElementById('btn-activate-yes'); let current = null; // { resource, id, name, section } let bridgeListener = null; @@ -49,6 +54,9 @@ export function initEditor() { let suppressDirty = false; let suppressTimer = null; let baselineReady = false; + let versionMap = new Map(); + let currentVersionId = 0; + let currentVersionMeta = null;  const ok = (m) => toast(m, true);  const err = (m) => toast(m, false); @@ -94,6 +102,7 @@ export function initEditor() { function setVersionUiVisible(show) { if (versionSelect) versionSelect.classList.toggle('hidden', !show); // restore button removed + if (btnDeactivateVersion) btnDeactivateVersion.classList.toggle('hidden', !show); } setVersionUiVisible(false); @@ -262,11 +271,49 @@ export function initEditor() { return choice; } + function showActivateDialog() { + return new Promise((resolve) => { + if (!activateDialog || typeof activateDialog.showModal !== 'function') { + resolve('no'); + return; + } + const cleanup = () => { + btnActivateCancel && btnActivateCancel.removeEventListener('click', onCancel); + btnActivateNo && btnActivateNo.removeEventListener('click', onNo); + btnActivateYes && btnActivateYes.removeEventListener('click', onYes); + }; + const closeWith = (choice) => { + cleanup(); + activateDialog.close(); + resolve(choice); + }; + const onCancel = () => closeWith('cancel'); + const onNo = () => closeWith('no'); + const onYes = () => closeWith('yes'); + btnActivateCancel && btnActivateCancel.addEventListener('click', onCancel); + btnActivateNo && btnActivateNo.addEventListener('click', onNo); + btnActivateYes && btnActivateYes.addEventListener('click', onYes); + activateDialog.showModal(); + }); + } + + function updateVersionMeta(id) { + const key = id ? String(id) : ''; + currentVersionId = id ? Number(id) : 0; + currentVersionMeta = key && versionMap.has(key) ? versionMap.get(key) : null; + if (btnDeactivateVersion) { + const isActive = !!(currentVersionMeta && Number(currentVersionMeta.is_active) === 1); + btnDeactivateVersion.classList.toggle('hidden', !isActive); + } + } + function renderVersionOptions(items) { versionItems = items || []; - if (!versionSelect) return; + versionMap = new Map(); + if (!versionSelect) return ''; const rows = Array.isArray(versionItems) ? versionItems : []; versionSelect.innerHTML = ''; + lastVersionSelection = ''; if (!rows.length) { const opt = document.createElement('option'); opt.value = ''; @@ -274,33 +321,41 @@ export function initEditor() { opt.disabled = true; versionSelect.appendChild(opt); versionSelect.disabled = true; - return; + updateVersionMeta(0); + return ''; } versionSelect.disabled = false; - rows.forEach((item, idx) => { + let activeId = ''; + rows.forEach((item) => { const opt = document.createElement('option'); - const label = `#${item.version_no} – ${formatVersionDate(item.created_at)}`; + const label = `#${item.version_no} – ${formatVersionDate(item.created_at)}` + (Number(item.is_active) === 1 ? ' (aktiv)' : ''); opt.value = String(item.id); opt.textContent = label; versionSelect.appendChild(opt); - if (!lastVersionSelection && idx === 0) { - lastVersionSelection = String(item.id); - } + versionMap.set(String(item.id), item); + if (Number(item.is_active) === 1 && !activeId) activeId = String(item.id); }); - if (lastVersionSelection) versionSelect.value = lastVersionSelection; + const fallbackId = activeId || (rows[0] ? String(rows[0].id) : ''); + if (fallbackId) { + lastVersionSelection = fallbackId; + versionSelect.value = fallbackId; + updateVersionMeta(Number(fallbackId)); + } + return lastVersionSelection; } async function loadVersionsForCurrent() { if (!current?.id) { renderVersionOptions([]); - return; + return ''; } try { const res = await apiAction('content_versions.list', { method: 'GET', data: { content_id: current.id, id: current.id } }); if (!res?.ok) throw new Error(res?.error || 'Versionen konnten nicht geladen werden'); - renderVersionOptions(Array.isArray(res?.items) ? res.items : []); + return renderVersionOptions(Array.isArray(res?.items) ? res.items : []); } catch { renderVersionOptions([]); + return ''; } } @@ -594,9 +649,11 @@ export function initEditor() {   // Overlay zeigen   showVeil(); -  // Daten parallel laden (fresh HTML + kontextgefilterte Snippets + Referenzen) -  let fresh = ''; -  let snippets = []; + const requestedVersionId = Number(item?.version_id || item?.versionId || 0); + + // Daten parallel laden (fresh HTML + kontextgefilterte Snippets + Referenzen) + let fresh = ''; + let snippets = []; let refLib = { sections: [], blocks: [] }; let hasJson = false; let jsonState = ''; @@ -604,6 +661,7 @@ export function initEditor() { let editorType = 'grapesjs'; let craftJson = ''; + let defaultVersionId = '';   await Promise.all([    (async() => { try { @@ -624,9 +682,25 @@ export function initEditor() { })(),    (async() => { snippets = await buildSnippetsForContext(current); })(),    (async() => { refLib  = await buildRefLibForContext(current); })(), - (async() => { await loadVersionsForCurrent(); })() + (async() => { defaultVersionId = await loadVersionsForCurrent(); })()   ]); + const effectiveVersionId = requestedVersionId ? String(requestedVersionId) : defaultVersionId; + if (effectiveVersionId) { + try { + const res = await apiAction('content_versions.get', { method: 'GET', data: { id: effectiveVersionId, content_id: current.id } }); + if (res?.ok && res?.item) { + const fields = extractContentFields(res.item); + fresh = fields.html || ''; + jsonState = fields.json || ''; + editorType = fields.editorType || editorType; + craftJson = fields.craftJson || ''; + updateVersionMeta(Number(effectiveVersionId)); + lastVersionSelection = String(effectiveVersionId); + } + } catch {} + } + editorType = editorType === 'craftjs' ? 'craftjs' : 'grapesjs'; setSavedSnapshotFromData({ html: fresh, content: jsonState, editor_type: editorType, craft_json: craftJson }); setEditorType(editorType); @@ -744,12 +818,20 @@ export function initEditor() { async function save() { if (!current?.id) return err('Keine aktive ID'); + let activateNext = false; + if (currentVersionMeta && (Number(currentVersionMeta.is_active) === 1 || Number(currentVersionMeta.was_active) === 1)) { + const decision = await showActivateDialog(); + if (decision === 'cancel') return false; + activateNext = decision === 'yes'; + } + 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', section_id: current.section.id }; + if (activateNext) payload.activate_version = 1; const res = await apiUpdate('content', current.id, payload); if (res?.ok) ok('Gespeichert'); else err(res?.error || 'Speichern fehlgeschlagen'); @@ -757,6 +839,14 @@ export function initEditor() { return res?.ok; } + if (activateNext) { + const win = iframe?.contentWindow; + if (win && win.BridgeParts) { + win.BridgeParts.NEXT_ACTIVATE_VERSION = 1; + } else if (win) { + win.NEXT_ACTIVATE_VERSION = 1; + } + } const okSave = await delegateCommand('save-data'); if (okSave) { setTimeout(async () => { @@ -921,6 +1011,18 @@ export function initEditor() { btnCancelSend&& (btnCancelSend.onclick= closeSend); sendForm && (sendForm.onsubmit = doSend); editorSelect && (editorSelect.onchange = () => switchEditor(editorSelect.value)); + btnDeactivateVersion && (btnDeactivateVersion.onclick = async () => { + if (!current?.id) return; + if (!currentVersionMeta || Number(currentVersionMeta.is_active) !== 1) return; + try { + const res = await apiAction('content_versions.deactivate', { method: 'POST', data: { content_id: current.id } }); + if (!res?.ok) throw new Error(res?.error || 'Deaktivieren fehlgeschlagen'); + await loadVersionsForCurrent(); + toast('Aktive Version deaktiviert', true); + } catch (e) { + err(e.message || 'Deaktivieren fehlgeschlagen'); + } + }); versionSelect && (versionSelect.onchange = async () => { if (!current?.id) return; const previousSelection = lastVersionSelection; @@ -939,6 +1041,7 @@ export function initEditor() { if (!res?.ok) throw new Error(res?.error || 'Version konnte nicht geladen werden'); await applyVersionPayload(res?.item || res); lastVersionSelection = String(versionId); + updateVersionMeta(versionId); } catch (e) { err(e.message || 'Version konnte nicht geladen werden'); versionSelect.value = previousSelection; diff --git a/public/assets/js/ui-list.js b/public/assets/js/ui-list.js index aac5673..547ed3d 100644 --- a/public/assets/js/ui-list.js +++ b/public/assets/js/ui-list.js @@ -29,6 +29,7 @@ async function fetchContentItem(id, sectionId) { } async function openContentEditor(item, section) { + const versionId = Number(item?.version_id || 0); const id = Number(item?.id || 0); const name = item?.name || ''; if (!id) return; @@ -39,9 +40,9 @@ async function openContentEditor(item, section) { window.__currentEditorCtx = { id, mode: section.slug, section }; if (window.EditorUI && typeof window.EditorUI.open === 'function') { - window.EditorUI.open({ id, name, html, section }, 'content'); + window.EditorUI.open({ id, name, html, section, version_id: versionId }, 'content'); } else if (window.__openEditor) { - window.__openEditor({ resource: 'content', id, name, html, section }); + window.__openEditor({ resource: 'content', id, name, html, section, version_id: versionId }); } else { toast('Editor ist nicht initialisiert.', false); } @@ -189,11 +190,56 @@ export async function loadList(section) { return name.includes(q) || (api && api.includes(q)); } + const versionCache = new Map(); + + async function loadVersionOptions(selectEl, itemId) { + if (!selectEl || !itemId) return; + if (versionCache.has(itemId)) { + const cached = versionCache.get(itemId); + renderVersionSelect(selectEl, cached.items, cached.activeId); + return; + } + try { + const res = await apiAction('content_versions.list', { method: 'GET', data: { content_id: itemId } }); + const items = Array.isArray(res?.items) ? res.items : []; + const active = items.find(v => Number(v.is_active) === 1); + const activeId = active ? String(active.id) : (items[0] ? String(items[0].id) : ''); + versionCache.set(itemId, { items, activeId }); + renderVersionSelect(selectEl, items, activeId); + } catch { + renderVersionSelect(selectEl, [], ''); + } + } + + function renderVersionSelect(selectEl, items, activeId) { + selectEl.innerHTML = ''; + if (!items.length) { + const opt = document.createElement('option'); + opt.value = ''; + opt.textContent = 'Keine Versionen'; + opt.disabled = true; + selectEl.appendChild(opt); + selectEl.disabled = true; + return; + } + selectEl.disabled = false; + items.forEach(item => { + const opt = document.createElement('option'); + opt.value = String(item.id); + opt.textContent = `#${item.version_no}` + (Number(item.is_active) === 1 ? ' (aktiv)' : ''); + selectEl.appendChild(opt); + }); + if (activeId) selectEl.value = activeId; + } + function render(items) { list.innerHTML = items.map(item => { const name = esc(item.name || ''); const apiName = isTemplate ? esc(item.api_name || '') : ''; const apiLine = (isTemplate && apiName) ? `