mirror of
https://github.com/screentinker/screentinker.git
synced 2026-06-14 18:22:46 -06:00
Merge pull request #53 from screentinker/fix/template-zone-duplication
fix(layouts): atomic zone save — stop template zone duplication
This commit is contained in:
commit
3f429aec85
|
|
@ -293,15 +293,21 @@ async function renderEditor(container, layoutId) {
|
|||
|
||||
document.getElementById('saveLayoutBtn').onclick = async () => {
|
||||
try {
|
||||
for (const z of layout.zones || []) {
|
||||
await API(`/layouts/${layoutId}/zones/${z.id}`, { method: 'DELETE' });
|
||||
}
|
||||
for (const z of zones) {
|
||||
await API(`/layouts/${layoutId}/zones`, { method: 'POST', body: JSON.stringify(z) });
|
||||
}
|
||||
// Single atomic update: send the full zone set and the server replaces them
|
||||
// exactly. The old per-zone delete-then-add loop could accumulate zones
|
||||
// (and regenerated every zone id each save). Keep each zone's id so
|
||||
// device->zone assignments survive.
|
||||
const updated = await API(`/layouts/${layoutId}`, {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify({ zones }),
|
||||
});
|
||||
if (updated && updated.error) { showToast(updated.error, 'error'); return; }
|
||||
layout = updated;
|
||||
zones = layout.zones || [];
|
||||
selectedZone = null;
|
||||
showToast(t('layout.toast.saved'), 'success');
|
||||
layout = await API(`/layouts/${layoutId}`);
|
||||
zones = layout.zones;
|
||||
renderZones();
|
||||
updateProperties();
|
||||
} catch (err) {
|
||||
showToast(err.message, 'error');
|
||||
}
|
||||
|
|
|
|||
|
|
@ -719,6 +719,40 @@ function pruneScreenshots(deviceId) {
|
|||
`).run(deviceId, deviceId);
|
||||
}
|
||||
|
||||
// De-duplicate built-in template zones. A prior layout-editor save regenerated
|
||||
// every zone id on save; schema.sql's INSERT OR IGNORE then re-seeded the
|
||||
// canonical zone on the next boot, so template layouts accumulated positional
|
||||
// duplicates (e.g. a 2-zone split template grew to 4+). For each position in a
|
||||
// template, keep ONE zone, preferring the canonical seeded id (the built-in
|
||||
// template zones use 'z-...' ids; bug copies are uuids) so schema.sql's re-seed
|
||||
// stays an idempotent no-op; tiebreak by earliest rowid. One-time; the atomic
|
||||
// id-preserving save prevents recurrence.
|
||||
try {
|
||||
const DEDUPE_ID = 'dedupe_template_zones_v1';
|
||||
if (!db.prepare('SELECT 1 FROM schema_migrations WHERE id = ?').get(DEDUPE_ID)) {
|
||||
const removed = db.prepare(`
|
||||
DELETE FROM layout_zones WHERE id IN (
|
||||
SELECT z.id FROM layout_zones z
|
||||
JOIN layouts l ON l.id = z.layout_id
|
||||
WHERE l.is_template = 1 AND EXISTS (
|
||||
SELECT 1 FROM layout_zones z2
|
||||
WHERE z2.layout_id = z.layout_id AND z2.id != z.id
|
||||
AND z2.x_percent = z.x_percent AND z2.y_percent = z.y_percent
|
||||
AND z2.width_percent = z.width_percent AND z2.height_percent = z.height_percent
|
||||
AND (
|
||||
-- z2 is canonical and z is not -> keep z2, drop z
|
||||
(z2.id LIKE 'z-%' AND z.id NOT LIKE 'z-%')
|
||||
-- same canonical-ness -> keep the earliest, drop the rest
|
||||
OR ((CASE WHEN z2.id LIKE 'z-%' THEN 1 ELSE 0 END) = (CASE WHEN z.id LIKE 'z-%' THEN 1 ELSE 0 END) AND z2.rowid < z.rowid)
|
||||
)
|
||||
)
|
||||
)
|
||||
`).run().changes;
|
||||
if (removed > 0) console.log(`[migrate] removed ${removed} duplicate template zone(s)`);
|
||||
db.prepare('INSERT OR IGNORE INTO schema_migrations (id) VALUES (?)').run(DEDUPE_ID);
|
||||
}
|
||||
} catch (e) { console.error('[migrate] template-zone dedupe failed:', e.message); }
|
||||
|
||||
// #37: fail fast (loud) if migrations left the DB missing schema the code needs.
|
||||
const { verifyAndRepairSchema } = require('../lib/schema-check');
|
||||
verifyAndRepairSchema(db);
|
||||
|
|
|
|||
|
|
@ -116,10 +116,32 @@ router.put('/:id', (req, res) => {
|
|||
if (!layout) return;
|
||||
if (layout.is_template && !PLATFORM_ROLES.includes(req.user.role)) return res.status(403).json({ error: 'Cannot edit templates' });
|
||||
|
||||
const { name, width, height } = req.body;
|
||||
if (name) db.prepare('UPDATE layouts SET name = ?, updated_at = strftime(\'%s\',\'now\') WHERE id = ?').run(name, req.params.id);
|
||||
if (width) db.prepare('UPDATE layouts SET width = ? WHERE id = ?').run(width, req.params.id);
|
||||
if (height) db.prepare('UPDATE layouts SET height = ? WHERE id = ?').run(height, req.params.id);
|
||||
const { name, width, height, zones } = req.body;
|
||||
const txn = db.transaction(() => {
|
||||
if (name) db.prepare('UPDATE layouts SET name = ?, updated_at = strftime(\'%s\',\'now\') WHERE id = ?').run(name, req.params.id);
|
||||
if (width) db.prepare('UPDATE layouts SET width = ? WHERE id = ?').run(width, req.params.id);
|
||||
if (height) db.prepare('UPDATE layouts SET height = ? WHERE id = ?').run(height, req.params.id);
|
||||
|
||||
// Atomic zone replace: the editor sends the FULL desired set, so the layout
|
||||
// ends up with EXACTLY those zones - no accumulation from a per-zone
|
||||
// delete/add loop. Reuse each zone's id when supplied so device->zone
|
||||
// assignments survive an edit (a fresh uuid per save would orphan them).
|
||||
if (Array.isArray(zones)) {
|
||||
db.prepare('DELETE FROM layout_zones WHERE layout_id = ?').run(req.params.id);
|
||||
const stmt = db.prepare(`
|
||||
INSERT INTO layout_zones (id, layout_id, name, x_percent, y_percent, width_percent, height_percent, z_index, zone_type, fit_mode, background_color, sort_order)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`);
|
||||
zones.forEach((z, i) => {
|
||||
stmt.run(z.id || uuidv4(), req.params.id, z.name || `Zone ${i + 1}`,
|
||||
z.x_percent || 0, z.y_percent || 0, z.width_percent || 100, z.height_percent || 100,
|
||||
z.z_index || 0, z.zone_type || 'content', z.fit_mode || 'contain',
|
||||
z.background_color || '#000000', i);
|
||||
});
|
||||
db.prepare('UPDATE layouts SET updated_at = strftime(\'%s\',\'now\') WHERE id = ?').run(req.params.id);
|
||||
}
|
||||
});
|
||||
txn();
|
||||
|
||||
const updated = db.prepare('SELECT * FROM layouts WHERE id = ?').get(req.params.id);
|
||||
updated.zones = db.prepare('SELECT * FROM layout_zones WHERE layout_id = ? ORDER BY sort_order').all(req.params.id);
|
||||
|
|
|
|||
Loading…
Reference in a new issue