From b7729be6d988cae3fc5a19beee68ca3f0a19308b Mon Sep 17 00:00:00 2001 From: Lars Gebhardt-Kusche Date: Tue, 3 Feb 2026 23:59:17 +0100 Subject: [PATCH] sdyscxyc --- config/current.ver | 2 +- public/assets/js/ui-editor.js | 177 ++++++++++++++++++++++++++++------ src/ApiKernel.php | 32 ++++++ 3 files changed, 180 insertions(+), 31 deletions(-) diff --git a/config/current.ver b/config/current.ver index 717d98b..80f5e3a 100644 --- a/config/current.ver +++ b/config/current.ver @@ -1 +1 @@ -1.2.30 \ No newline at end of file +1.2.31 \ No newline at end of file diff --git a/public/assets/js/ui-editor.js b/public/assets/js/ui-editor.js index 253fb38..dc7061c 100644 --- a/public/assets/js/ui-editor.js +++ b/public/assets/js/ui-editor.js @@ -63,6 +63,42 @@ export function initEditor() { const okĀ  = (m) => toast(m, true); const err = (m) => toast(m, false); + let lastDebugTs = 0; + let lastDebugKey = ''; + let lastSelectionInfo = null; + + const debugUiLog = (payload) => { + const now = Date.now(); + const key = payload && payload.event ? payload.event : 'log'; + if (now - lastDebugTs < 400 && key === lastDebugKey) return; + lastDebugTs = now; + lastDebugKey = key; + apiAction('debug.log.write', { + method: 'POST', + data: { + name: 'ui_editor_dirty.log', + append: 1, + line: JSON.stringify({ + time: new Date().toISOString(), + ...payload, + }), + }, + }).catch(() => {}); + }; + + const isLibRefModel = (model) => { + if (!model) return false; + const attrs = typeof model.getAttributes === 'function' + ? model.getAttributes() + : (model.attributes && model.attributes.attributes) ? model.attributes.attributes : {}; + if (attrs && (attrs['data-lib-ref'] || attrs['data-lib-kind'] || attrs['data-lib-id'])) return true; + try { + const classes = typeof model.getClasses === 'function' ? model.getClasses() : []; + if (Array.isArray(classes) && classes.includes('lib-ref-wrapper')) return true; + } catch {} + return false; + }; + const showConfirmDialog = (() => { let dialog; let titleEl; @@ -216,9 +252,57 @@ export function initEditor() { }; } - function markDirty() { - if (suppressDirty || !baselineReady) return; + function getModelInfo(model) { + if (!model) return null; + const info = {}; + try { + info.id = (typeof model.getId === 'function' ? model.getId() : null) + ?? (typeof model.get === 'function' ? model.get('id') : null) + ?? model.id + ?? null; + info.type = (typeof model.get === 'function' ? model.get('type') : null) + ?? (typeof model.get === 'function' ? model.get('tagName') : null) + ?? (model.attributes ? model.attributes.type : null) + ?? null; + info.libRef = isLibRefModel(model); + if (info.libRef) { + const attrs = typeof model.getAttributes === 'function' + ? model.getAttributes() + : (model.attributes && model.attributes.attributes) ? model.attributes.attributes : {}; + if (attrs) { + info.lib = { + ref: attrs['data-lib-ref'] ?? null, + kind: attrs['data-lib-kind'] ?? null, + id: attrs['data-lib-id'] ?? null, + }; + } + } + } catch {} + return info; + } + + function markDirty(reason = 'unknown', model = null, meta = {}) { + const blocked = suppressDirty || !baselineReady; + if (blocked) { + debugUiLog({ + event: 'dirty:ignored', + reason, + suppressDirty, + baselineReady, + model: getModelInfo(model), + meta, + selection: lastSelectionInfo, + }); + return; + } isDirty = true; + debugUiLog({ + event: 'dirty:set', + reason, + model: getModelInfo(model), + meta, + selection: lastSelectionInfo, + }); } function clearDirty() { @@ -238,53 +322,86 @@ export function initEditor() { if (!editor || typeof editor.on !== 'function') return () => {}; let selectionJustChanged = false; let selectionTimer = null; - const isLibRef = (model) => { - if (!model) return false; - const attrs = typeof model.getAttributes === 'function' - ? model.getAttributes() - : (model.attributes && model.attributes.attributes) ? model.attributes.attributes : {}; - if (attrs && (attrs['data-lib-ref'] || attrs['data-lib-kind'] || attrs['data-lib-id'])) return true; - try { - const classes = typeof model.getClasses === 'function' ? model.getClasses() : []; - if (Array.isArray(classes) && classes.includes('lib-ref-wrapper')) return true; - } catch {} - return false; - }; - const onSelect = () => { + const onSelect = (model) => { selectionJustChanged = true; + lastSelectionInfo = getModelInfo(model); + debugUiLog({ + event: 'component:selected', + selection: lastSelectionInfo, + }); if (selectionTimer) clearTimeout(selectionTimer); selectionTimer = setTimeout(() => { selectionJustChanged = false; selectionTimer = null; }, 800); }; - const onUpdate = () => { - if (selectionJustChanged) return; - markDirty(); + const onUpdate = (reason, model) => { + if (selectionJustChanged) { + debugUiLog({ + event: 'dirty:skip', + reason, + note: 'selectionJustChanged', + model: getModelInfo(model), + }); + return; + } + markDirty(reason, model); }; const onComponentUpdate = (model) => { - if (isLibRef(model)) return; + if (isLibRefModel(model)) { + debugUiLog({ + event: 'component:update:skip', + reason: 'lib-ref', + model: getModelInfo(model), + }); + return; + } const changed = (model && typeof model.changedAttributes === 'function') ? model.changedAttributes() : (model && model.changed) ? model.changed : null; const keys = changed ? Object.keys(changed) : []; - if (!keys.length && selectionJustChanged) return; + if (!keys.length && selectionJustChanged) { + debugUiLog({ + event: 'component:update:skip', + reason: 'selectionJustChanged', + model: getModelInfo(model), + }); + return; + } const safeKeys = new Set(['status', 'toolbar', 'selected', 'hovered', 'highlighted', 'hoverable', 'selectable', 'editable', 'draggable', 'droppable', 'copyable', 'removable', 'locked']); - if (keys.length && keys.every(k => safeKeys.has(k))) return; - if (selectionJustChanged && keys.length === 0) return; - markDirty(); + if (keys.length && keys.every(k => safeKeys.has(k))) { + debugUiLog({ + event: 'component:update:skip', + reason: 'safeKeysOnly', + keys, + model: getModelInfo(model), + }); + return; + } + if (selectionJustChanged && keys.length === 0) { + debugUiLog({ + event: 'component:update:skip', + reason: 'selectionJustChangedNoKeys', + model: getModelInfo(model), + }); + return; + } + markDirty('component:update', model, { keys }); }; + const onComponentAdd = (model) => onUpdate('component:add', model); + const onComponentRemove = (model) => onUpdate('component:remove', model); + const onStyleUpdate = (model) => onUpdate('style:property:update', model); editor.on('component:update', onComponentUpdate); - editor.on('component:add', onUpdate); - editor.on('component:remove', onUpdate); - editor.on('style:property:update', onUpdate); + editor.on('component:add', onComponentAdd); + editor.on('component:remove', onComponentRemove); + editor.on('style:property:update', onStyleUpdate); editor.on('component:selected', onSelect); return () => { try { editor.off('component:update', onComponentUpdate); - editor.off('component:add', onUpdate); - editor.off('component:remove', onUpdate); - editor.off('style:property:update', onUpdate); + editor.off('component:add', onComponentAdd); + editor.off('component:remove', onComponentRemove); + editor.off('style:property:update', onStyleUpdate); editor.off('component:selected', onSelect); } catch {} }; @@ -293,7 +410,7 @@ export function initEditor() { function attachCraftDirtyTracker() { const host = document.getElementById('craftEditor'); if (!host) return () => {}; - const handler = () => markDirty(); + const handler = () => markDirty('craft:input'); const events = ['input', 'keydown', 'paste', 'drop']; events.forEach(evt => host.addEventListener(evt, handler, true)); return () => { diff --git a/src/ApiKernel.php b/src/ApiKernel.php index 5bd6d78..5511020 100644 --- a/src/ApiKernel.php +++ b/src/ApiKernel.php @@ -2987,6 +2987,9 @@ class ApiKernel case 'debug.logs.read': $this->handleDebugLogsRead(); break; + case 'debug.log.write': + $this->handleDebugLogWrite(); + break; case 'placeholders.status': $this->handlePlaceholderStatus(); break; @@ -4249,6 +4252,35 @@ class ApiKernel $this->respond(['ok' => true, 'content' => $content]); } + private function handleDebugLogWrite(): void + { + $user = $this->requireAuth(); + $this->ensureDebugUser($user); + $this->ensureDebugEnv(); + $name = trim((string)($this->in['name'] ?? 'ui_editor_dirty.log')); + $line = $this->in['line'] ?? ''; + $append = (int)($this->in['append'] ?? 1) === 1; + if ($name === '') { + $this->fail('Log name required', null, 422); + } + $name = preg_replace('/[^a-zA-Z0-9_\.\-]/', '', $name) ?: 'debug.log'; + if (strpos($name, '..') !== false) { + $this->fail('Invalid log name', null, 422); + } + $payload = $line; + if (is_array($payload) || is_object($payload)) { + $payload = json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); + } else { + $payload = (string)$payload; + } + debug_log_write($name, $payload, [ + 'append' => $append, + 'json' => false, + 'newline' => true, + ]); + $this->respond(['ok' => true, 'name' => $name]); + } + private function resolveBridgeConfig(?int $customerId): array { $fileConf = $this->conf['placeholders']['bridge'] ?? [];