diff --git a/display/frontend/src/pages/Layouts.tsx b/display/frontend/src/pages/Layouts.tsx index 807c8f1..f342bf3 100644 --- a/display/frontend/src/pages/Layouts.tsx +++ b/display/frontend/src/pages/Layouts.tsx @@ -137,6 +137,31 @@ export default function Layouts() { } catch (e) { setError((e as Error).message); } } + function handleExport() { + if (!selected) return; + window.open(`/api/layouts/${selected.id}/export`, '_blank'); + } + + async function handleImport() { + const input = document.createElement('input'); + input.type = 'file'; + input.accept = '.json'; + input.onchange = async (e) => { + const file = (e.target as HTMLInputElement).files?.[0]; + if (!file) return; + try { + const text = await file.text(); + const data = JSON.parse(text); + const result = await api.post('/api/layouts/import', data); + loadLayouts(); + selectLayout(result.id); + } catch (e) { + setError((e as Error).message); + } + }; + input.click(); + } + function NumInput({ value, onChange, label, w }: { value: number; onChange: (v: number) => void; label: string; w?: string }) { return (
@@ -165,10 +190,13 @@ export default function Layouts() {
))} -
+ setNewName(e.target.value)} />
+ {/* Right: element editor */} @@ -183,6 +211,7 @@ export default function Layouts() { + diff --git a/display/middleware/src/api/routes/layouts.ts b/display/middleware/src/api/routes/layouts.ts index 6a08db4..9f17625 100644 --- a/display/middleware/src/api/routes/layouts.ts +++ b/display/middleware/src/api/routes/layouts.ts @@ -64,6 +64,113 @@ router.delete('/:id', (req, res): void => { } }); +// ---- Export / Import ---- + +// GET /api/layouts/:id/export — export layout with elements as JSON file +router.get('/:id/export', (req, res): void => { + try { + const layout = getLayoutById(parseInt(req.params.id)); + if (!layout) { res.status(404).json({ error: 'Layout not found' }); return; } + + const exportData = { + _format: 'epaper-layout-v1', + name: layout.name, + type: layout.type, + width: layout.width, + height: layout.height, + grid_cols: layout.grid_cols, + grid_rows: layout.grid_rows, + elements: layout.elements.map((el: any) => ({ + type: el.type, + x: el.x, + y: el.y, + width: el.width, + height: el.height, + font_size: el.font_size, + font_weight: el.font_weight, + text_align: el.text_align, + icon: el.icon, + style: el.style, + static_text: el.static_text, + grid_col: el.grid_col, + grid_row: el.grid_row, + sort_order: el.sort_order, + // Store datapoint name for re-linking on import + datapoint_name: el.datapoint_id + ? (require('../../modules/datapoints/datapoint-manager').getDatapointById(el.datapoint_id)?.name || null) + : null, + })), + }; + + const filename = `layout-${layout.name.replace(/[^a-zA-Z0-9_-]/g, '_')}.json`; + res.setHeader('Content-Disposition', `attachment; filename="${filename}"`); + res.setHeader('Content-Type', 'application/json'); + res.json(exportData); + } catch (err) { + res.status(500).json({ error: (err as Error).message }); + } +}); + +// POST /api/layouts/import — import layout from JSON +router.post('/import', (req, res): void => { + try { + const data = req.body; + if (!data._format || !data._format.startsWith('epaper-layout')) { + res.status(400).json({ error: 'Ungueltiges Layout-Format. Erwartet: epaper-layout-v1' }); + return; + } + if (!data.name || !data.elements) { + res.status(400).json({ error: 'name und elements sind erforderlich' }); + return; + } + + // Create layout + const layout = createLayout({ + name: data.name + ' (Import)', + type: data.type || 'free', + width: data.width, + height: data.height, + grid_cols: data.grid_cols, + grid_rows: data.grid_rows, + }); + + const { getDatapointByName } = require('../../modules/datapoints/datapoint-manager'); + + // Add elements + for (const el of data.elements) { + // Try to re-link datapoint by name + let datapointId = null; + if (el.datapoint_name) { + const dp = getDatapointByName ? getDatapointByName(el.datapoint_name) : null; + if (dp) datapointId = dp.id; + } + + addElement(layout.id, { + type: el.type || 'label', + x: el.x || 0, + y: el.y || 0, + width: el.width || null, + height: el.height || null, + font_size: el.font_size || 24, + font_weight: el.font_weight || 'normal', + text_align: el.text_align || 'left', + icon: el.icon || null, + style: el.style || 'plain', + static_text: el.static_text || null, + grid_col: el.grid_col || null, + grid_row: el.grid_row || null, + sort_order: el.sort_order || 0, + datapoint_id: datapointId, + }); + } + + const result = getLayoutById(layout.id); + res.status(201).json(result); + } catch (err) { + res.status(500).json({ error: (err as Error).message }); + } +}); + // ---- Elements ---- // POST /api/layouts/:layoutId/elements diff --git a/display/middleware/src/modules/datapoints/datapoint-manager.ts b/display/middleware/src/modules/datapoints/datapoint-manager.ts index 39aa756..e3685d8 100644 --- a/display/middleware/src/modules/datapoints/datapoint-manager.ts +++ b/display/middleware/src/modules/datapoints/datapoint-manager.ts @@ -118,6 +118,11 @@ export function createDatapointFromPg( return db.prepare('SELECT * FROM datapoints WHERE id = ?').get(id) as Datapoint; } +export function getDatapointByName(name: string): Datapoint | undefined { + const db = getDb(); + return db.prepare('SELECT * FROM datapoints WHERE name = ?').get(name) as Datapoint | undefined; +} + export function deleteDatapoint(id: number): boolean { const db = getDb(); const result = db.prepare('DELETE FROM datapoints WHERE id = ?').run(id);