This commit is contained in:
2026-02-03 23:59:17 +01:00
parent e282004786
commit b7729be6d9
3 changed files with 180 additions and 31 deletions

View File

@@ -1 +1 @@
1.2.30
1.2.31

View File

@@ -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 () => {

View File

@@ -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'] ?? [];