feat(editor): add section settings property editor API and sidebar modal
This commit is contained in:
@@ -65,10 +65,16 @@ const previewUrl = `${targetPage?.slug || '/'}?preview=draft`;
|
|||||||
<label class="text-xs font-semibold text-slate-400 uppercase tracking-wider block">Sektionen verwalten</label>
|
<label class="text-xs font-semibold text-slate-400 uppercase tracking-wider block">Sektionen verwalten</label>
|
||||||
<div id="section-list" class="space-y-2">
|
<div id="section-list" class="space-y-2">
|
||||||
{targetPage?.sections?.map((sec: any, index: number) => (
|
{targetPage?.sections?.map((sec: any, index: number) => (
|
||||||
<div class="p-3 bg-slate-800/80 border border-slate-700/60 rounded-lg text-xs space-y-2 text-slate-200" data-section-id={sec.id}>
|
<div
|
||||||
|
class="p-3 bg-slate-800/80 border border-slate-700/60 rounded-lg text-xs space-y-2 text-slate-200"
|
||||||
|
data-section-id={sec.id}
|
||||||
|
data-section-type={sec.type}
|
||||||
|
data-settings={JSON.stringify(sec.settings || {})}
|
||||||
|
>
|
||||||
<div class="flex items-center justify-between font-semibold">
|
<div class="flex items-center justify-between font-semibold">
|
||||||
<span class="text-sky-400">#{index + 1} {sec.type}</span>
|
<span class="text-sky-400">#{index + 1} {sec.type}</span>
|
||||||
<div class="flex items-center gap-1">
|
<div class="flex items-center gap-1">
|
||||||
|
<button class="btn-edit-section hover:text-sky-400 px-1" title="Inhalte/Einstellungen bearbeiten">✏️</button>
|
||||||
<button class="btn-move-up hover:text-sky-400 px-1" title="Nach oben">⬆️</button>
|
<button class="btn-move-up hover:text-sky-400 px-1" title="Nach oben">⬆️</button>
|
||||||
<button class="btn-move-down hover:text-sky-400 px-1" title="Nach unten">⬇️</button>
|
<button class="btn-move-down hover:text-sky-400 px-1" title="Nach unten">⬇️</button>
|
||||||
<button class="btn-delete-section hover:text-red-400 px-1" title="Sektion löschen">🗑️</button>
|
<button class="btn-delete-section hover:text-red-400 px-1" title="Sektion löschen">🗑️</button>
|
||||||
@@ -104,6 +110,24 @@ const previewUrl = `${targetPage?.slug || '/'}?preview=draft`;
|
|||||||
</main>
|
</main>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<dialog id="modal-edit-section" class="bg-slate-900 text-white border border-slate-800 rounded-xl p-6 backdrop:bg-slate-950/80 max-w-lg w-full">
|
||||||
|
<form id="form-edit-section-settings" class="space-y-4">
|
||||||
|
<h2 class="text-lg font-bold text-white flex items-center justify-between">
|
||||||
|
<span>Sektion bearbeiten</span>
|
||||||
|
<span id="modal-section-type-badge" class="text-xs font-mono bg-sky-950 text-sky-400 border border-sky-800 px-2 py-0.5 rounded"></span>
|
||||||
|
</h2>
|
||||||
|
<input type="hidden" id="edit-section-id" name="section_id" />
|
||||||
|
|
||||||
|
<div id="dynamic-settings-fields" class="space-y-3">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex justify-end gap-2 pt-4 border-t border-slate-800">
|
||||||
|
<button type="button" id="btn-close-edit-modal" class="px-4 py-2 bg-slate-800 text-slate-300 rounded-lg text-xs">Abbrechen</button>
|
||||||
|
<button type="submit" class="px-4 py-2 bg-sky-500 text-slate-950 font-bold rounded-lg text-xs">Übernehmen & Vorschau</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</dialog>
|
||||||
</Layout>
|
</Layout>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
@@ -182,4 +206,69 @@ const previewUrl = `${targetPage?.slug || '/'}?preview=draft`;
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 7. Event-Handling für das Öffnen & Speichern von Sektions-Eigenschaften
|
||||||
|
const editModal = document.getElementById('modal-edit-section') as HTMLDialogElement;
|
||||||
|
const fieldsContainer = document.getElementById('dynamic-settings-fields');
|
||||||
|
|
||||||
|
document.getElementById('btn-close-edit-modal')?.addEventListener('click', () => editModal?.close());
|
||||||
|
|
||||||
|
// Klick auf "Bearbeiten" an einer Sektion
|
||||||
|
document.querySelectorAll('.btn-edit-section')?.forEach(btn => {
|
||||||
|
btn.addEventListener('click', (e: any) => {
|
||||||
|
const card = e.target.closest('[data-section-id]');
|
||||||
|
const sectionId = card.getAttribute('data-section-id');
|
||||||
|
const sectionType = card.getAttribute('data-section-type');
|
||||||
|
const currentSettings = JSON.parse(card.getAttribute('data-settings') || '{}');
|
||||||
|
|
||||||
|
(document.getElementById('edit-section-id') as HTMLInputElement).value = sectionId;
|
||||||
|
(document.getElementById('modal-section-type-badge') as HTMLElement).textContent = sectionType;
|
||||||
|
|
||||||
|
if (fieldsContainer) {
|
||||||
|
fieldsContainer.innerHTML = '';
|
||||||
|
Object.entries(currentSettings).forEach(([key, value]) => {
|
||||||
|
const fieldWrapper = document.createElement('div');
|
||||||
|
fieldWrapper.innerHTML = `
|
||||||
|
<label class="block text-xs font-semibold text-slate-400 mb-1 uppercase tracking-wider">${key}</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
name="${key}"
|
||||||
|
value="${value}"
|
||||||
|
class="w-full bg-slate-800 border border-slate-700 rounded-lg p-2 text-xs text-white focus:outline-none focus:border-sky-500"
|
||||||
|
/>
|
||||||
|
`;
|
||||||
|
fieldsContainer.appendChild(fieldWrapper);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
editModal?.showModal();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Submit der Sektions-Einstellungen
|
||||||
|
document.getElementById('form-edit-section-settings')?.addEventListener('submit', async (e: Event) => {
|
||||||
|
e.preventDefault();
|
||||||
|
const formData = new FormData(e.target as HTMLFormElement);
|
||||||
|
const sectionId = formData.get('section_id') as string;
|
||||||
|
formData.delete('section_id');
|
||||||
|
|
||||||
|
const settings: Record<string, any> = {};
|
||||||
|
formData.forEach((val, key) => { settings[key] = val; });
|
||||||
|
|
||||||
|
const urlParams = new URLSearchParams(window.location.search);
|
||||||
|
const pageId = urlParams.get('page_id');
|
||||||
|
|
||||||
|
const res = await fetch('/api/editor/update-section-settings', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ page_id: pageId, section_id: sectionId, settings })
|
||||||
|
});
|
||||||
|
|
||||||
|
if (res.ok) {
|
||||||
|
editModal?.close();
|
||||||
|
// Schicke Live-Reload Signal an Iframe Bridge
|
||||||
|
const iframeElement = document.getElementById('preview-iframe') as HTMLIFrameElement;
|
||||||
|
iframeElement?.contentWindow?.postMessage({ type: 'RELOAD_CANVAS' }, '*');
|
||||||
|
}
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
49
src/pages/api/editor/update-section-settings.ts
Normal file
49
src/pages/api/editor/update-section-settings.ts
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
import type { APIRoute } from 'astro';
|
||||||
|
import fs from 'node:fs/promises';
|
||||||
|
import path from 'node:path';
|
||||||
|
|
||||||
|
export const prerender = false;
|
||||||
|
|
||||||
|
const DATA_DIR = process.env.DATA_DIR || path.join(process.cwd(), 'app', 'data');
|
||||||
|
|
||||||
|
export const POST: APIRoute = async ({ request }) => {
|
||||||
|
try {
|
||||||
|
const { page_id, section_id, settings } = await request.json();
|
||||||
|
|
||||||
|
if (!page_id || !section_id || !settings) {
|
||||||
|
return new Response(JSON.stringify({ error: 'page_id, section_id und settings erforderlich' }), { status: 400 });
|
||||||
|
}
|
||||||
|
|
||||||
|
const draftPath = path.join(DATA_DIR, 'site.config.draft.json');
|
||||||
|
const livePath = path.join(DATA_DIR, 'site.config.json');
|
||||||
|
|
||||||
|
let configRaw: string;
|
||||||
|
try {
|
||||||
|
configRaw = await fs.readFile(draftPath, 'utf-8');
|
||||||
|
} catch {
|
||||||
|
configRaw = await fs.readFile(livePath, 'utf-8');
|
||||||
|
}
|
||||||
|
|
||||||
|
const config = JSON.parse(configRaw);
|
||||||
|
const targetPage = config.pages?.find((p: any) => p.id === page_id);
|
||||||
|
|
||||||
|
if (!targetPage) {
|
||||||
|
return new Response(JSON.stringify({ error: 'Seite nicht gefunden' }), { status: 404 });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sektion finden und Einstellungen aktualisieren
|
||||||
|
const targetSection = targetPage.sections?.find((sec: any) => sec.id === section_id);
|
||||||
|
if (!targetSection) {
|
||||||
|
return new Response(JSON.stringify({ error: 'Sektion nicht gefunden' }), { status: 404 });
|
||||||
|
}
|
||||||
|
|
||||||
|
targetSection.settings = { ...targetSection.settings, ...settings };
|
||||||
|
|
||||||
|
await fs.mkdir(DATA_DIR, { recursive: true });
|
||||||
|
await fs.writeFile(draftPath, JSON.stringify(config, null, 2), 'utf-8');
|
||||||
|
|
||||||
|
return new Response(JSON.stringify({ success: true, settings: targetSection.settings }), { status: 200 });
|
||||||
|
} catch (error) {
|
||||||
|
return new Response(JSON.stringify({ error: 'Fehler beim Aktualisieren der Sektions-Einstellungen' }), { status: 500 });
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -2,9 +2,10 @@ import { describe, it, expect, beforeEach } from 'vitest';
|
|||||||
import fs from 'node:fs/promises';
|
import fs from 'node:fs/promises';
|
||||||
import path from 'node:path';
|
import path from 'node:path';
|
||||||
|
|
||||||
const DATA_DIR = process.env.DATA_DIR || path.join(process.cwd(), 'app', 'data');
|
const DATA_DIR = path.join(process.cwd(), 'app', 'test_data_sections');
|
||||||
const draftPath = path.join(DATA_DIR, 'site.config.draft.json');
|
const draftPath = path.join(DATA_DIR, 'site.config.draft.json');
|
||||||
|
|
||||||
|
|
||||||
describe('Editor Sections API Test', () => {
|
describe('Editor Sections API Test', () => {
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
await fs.mkdir(DATA_DIR, { recursive: true });
|
await fs.mkdir(DATA_DIR, { recursive: true });
|
||||||
|
|||||||
44
tests/editorSettings.test.ts
Normal file
44
tests/editorSettings.test.ts
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
import { describe, it, expect, beforeEach } from 'vitest';
|
||||||
|
import fs from 'node:fs/promises';
|
||||||
|
import path from 'node:path';
|
||||||
|
|
||||||
|
const DATA_DIR = path.join(process.cwd(), 'app', 'test_data_settings');
|
||||||
|
const draftPath = path.join(DATA_DIR, 'site.config.draft.json');
|
||||||
|
|
||||||
|
|
||||||
|
describe('Editor Property API Test', () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
await fs.mkdir(DATA_DIR, { recursive: true });
|
||||||
|
const dummyConfig = {
|
||||||
|
site_info: { title: 'Test Site', homepage_id: 'page_1' },
|
||||||
|
pages: [
|
||||||
|
{
|
||||||
|
id: 'page_1',
|
||||||
|
slug: '/',
|
||||||
|
title: 'Home',
|
||||||
|
sections: [
|
||||||
|
{ id: 'sec_hero_123', type: 'HeroSection', settings: { title: 'Alter Titel', subtitle: 'Alt' } }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
|
await fs.writeFile(draftPath, JSON.stringify(dummyConfig, null, 2), 'utf-8');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('soll Sektions-Settings im Entwurf erfolgreich aktualisieren', async () => {
|
||||||
|
const raw = await fs.readFile(draftPath, 'utf-8');
|
||||||
|
const config = JSON.parse(raw);
|
||||||
|
|
||||||
|
const hero = config.pages[0].sections[0];
|
||||||
|
hero.settings.title = 'Neuer Hero Titel';
|
||||||
|
hero.settings.subtitle = 'Neuer Untertitel';
|
||||||
|
|
||||||
|
await fs.writeFile(draftPath, JSON.stringify(config, null, 2), 'utf-8');
|
||||||
|
|
||||||
|
const updatedRaw = await fs.readFile(draftPath, 'utf-8');
|
||||||
|
const updatedConfig = JSON.parse(updatedRaw);
|
||||||
|
|
||||||
|
expect(updatedConfig.pages[0].sections[0].settings.title).toBe('Neuer Hero Titel');
|
||||||
|
expect(updatedConfig.pages[0].sections[0].settings.subtitle).toBe('Neuer Untertitel');
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user