mirror of
https://github.com/screentinker/screentinker.git
synced 2026-06-17 03:32:32 -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 () => {
|
document.getElementById('saveLayoutBtn').onclick = async () => {
|
||||||
try {
|
try {
|
||||||
for (const z of layout.zones || []) {
|
// Single atomic update: send the full zone set and the server replaces them
|
||||||
await API(`/layouts/${layoutId}/zones/${z.id}`, { method: 'DELETE' });
|
// 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
|
||||||
for (const z of zones) {
|
// device->zone assignments survive.
|
||||||
await API(`/layouts/${layoutId}/zones`, { method: 'POST', body: JSON.stringify(z) });
|
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');
|
showToast(t('layout.toast.saved'), 'success');
|
||||||
layout = await API(`/layouts/${layoutId}`);
|
renderZones();
|
||||||
zones = layout.zones;
|
updateProperties();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
showToast(err.message, 'error');
|
showToast(err.message, 'error');
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -719,6 +719,40 @@ function pruneScreenshots(deviceId) {
|
||||||
`).run(deviceId, 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.
|
// #37: fail fast (loud) if migrations left the DB missing schema the code needs.
|
||||||
const { verifyAndRepairSchema } = require('../lib/schema-check');
|
const { verifyAndRepairSchema } = require('../lib/schema-check');
|
||||||
verifyAndRepairSchema(db);
|
verifyAndRepairSchema(db);
|
||||||
|
|
|
||||||
|
|
@ -116,10 +116,32 @@ router.put('/:id', (req, res) => {
|
||||||
if (!layout) return;
|
if (!layout) return;
|
||||||
if (layout.is_template && !PLATFORM_ROLES.includes(req.user.role)) return res.status(403).json({ error: 'Cannot edit templates' });
|
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;
|
const { name, width, height, zones } = req.body;
|
||||||
if (name) db.prepare('UPDATE layouts SET name = ?, updated_at = strftime(\'%s\',\'now\') WHERE id = ?').run(name, req.params.id);
|
const txn = db.transaction(() => {
|
||||||
if (width) db.prepare('UPDATE layouts SET width = ? WHERE id = ?').run(width, req.params.id);
|
if (name) db.prepare('UPDATE layouts SET name = ?, updated_at = strftime(\'%s\',\'now\') WHERE id = ?').run(name, req.params.id);
|
||||||
if (height) db.prepare('UPDATE layouts SET height = ? WHERE id = ?').run(height, 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);
|
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);
|
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