From 79c3bb3a3eea926712df02b41f634c66a30ae280 Mon Sep 17 00:00:00 2001 From: Michael Date: Fri, 24 Apr 2026 16:55:11 +0200 Subject: [PATCH] =?UTF-8?q?fix:=20JSON-Editor=20Round-Trip=20f=C3=BCr=20ve?= =?UTF-8?q?rschachtelte=20Objekte/Arrays?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Rendering: innere Felder werden innerhalb der Objekt/Array-Container gerendert - Speichern: data-key-Pfade mit [index] werden korrekt zu Arrays rekonstruiert Fixes beide Symptome aus dem uncommitted Refactor. --- web/index.html | 164 ++++++++++++++++++++++++++++++++++++------------- 1 file changed, 121 insertions(+), 43 deletions(-) diff --git a/web/index.html b/web/index.html index 593ed31..9e13b56 100644 --- a/web/index.html +++ b/web/index.html @@ -597,45 +597,103 @@ $ python web/serve.py const formDiv = document.createElement('div'); formDiv.style.cssText = 'display: flex; flex-direction: column; gap: 16px; padding: 8px; background: var(--bg-card); border-radius: 4px; margin: 0; min-height: 300px; overflow-y: auto;'; + // Rekursive Funktion zum Erstellen von Eingabefeldern für alle Properties function buildJsonForm(data, prefix = '', level = 0) { + if (typeof data !== 'object' || data === null) return; + Object.keys(data).forEach(key => { const fullKey = prefix ? `${prefix}.${key}` : key; const value = data[key]; - const isObject = typeof value === 'object' && value !== null; + const isObject = typeof value === 'object' && value !== null && !Array.isArray(value); const isArray = Array.isArray(value); - if (isObject && !isArray) { + // Für Objekte: Rekursiv alle inneren Properties anzeigen + if (isObject) { + const fieldContainer = document.createElement('div'); + fieldContainer.style.cssText = 'background: #1a1a1a; padding: 12px; border-radius: 4px; border-left: 3px solid #4CAF50;'; + const label = document.createElement('label'); label.htmlFor = `edit-${fullKey.replace(/[.\s]/g, '-')}`; - label.textContent = fullKey + ' (Objekt) ♦'; - label.style.cssText = 'font-weight: 600; color: var(--text-primary); margin-bottom: 4px; font-size: 14px; margin-top: 8px;'; - formDiv.appendChild(label); + label.textContent = fullKey + ' (Objekt)'; + label.style.cssText = 'font-weight: 600; color: #4CAF50; margin-bottom: 8px; display: block; font-size: 14px;'; + fieldContainer.appendChild(label); - const input = document.createElement('textarea'); - input.value = JSON.stringify(value, null, 2); - input.dataset.key = fullKey; - input.rows = Object.keys(value).length > 10 ? 10 : Object.keys(value).length; - input.style.cssText = 'width: 100%; padding: 8px; background: #222222; color: #ffffff; border: 1px solid #cccccc; border-radius: 4px; font-family: var(--mono); font-size: 13px; resize: vertical;'; - formDiv.appendChild(input); + // Rekursiv innere Properties in den Container einfügen + const innerContainer = document.createElement('div'); + innerContainer.style.cssText = 'padding-left: 12px; margin-top: 4px;'; + buildJsonForm(value, fullKey, level + 1); + const existingChildren = Array.from(innerContainer.children); + while (innerContainer.firstChild) innerContainer.removeChild(innerContainer.firstChild); + existingChildren.forEach(ch => innerContainer.appendChild(ch)); + fieldContainer.appendChild(innerContainer); + + formDiv.appendChild(fieldContainer); return; } + // Für Arrays: Rekursiv jedes Element als eigenes Feld anzeigen if (isArray) { - const label = document.createElement('label'); - label.htmlFor = `edit-${fullKey.replace(/[.\s]/g, '-')}`; - label.textContent = fullKey + ' (Array) ♦'; - label.style.cssText = 'font-weight: 600; color: var(--text-primary); margin-bottom: 4px; font-size: 14px; margin-top: 8px;'; - formDiv.appendChild(label); + const arrayContainer = document.createElement('div'); + arrayContainer.style.cssText = 'background: #1a1a1a; padding: 12px; border-radius: 4px; border-left: 3px solid #2196F3;'; - const input = document.createElement('textarea'); - input.value = JSON.stringify(value); - input.dataset.key = fullKey; - input.rows = Math.min(value.length, 5); - input.style.cssText = 'width: 100%; padding: 8px; background: #222222; color: #ffffff; border: 1px solid #cccccc; border-radius: 4px; font-family: var(--mono); font-size: 13px; resize: vertical;'; - formDiv.appendChild(input); + const arrayLabel = document.createElement('label'); + arrayLabel.htmlFor = `edit-${fullKey.replace(/[.\s]/g, '-')}`; + arrayLabel.textContent = fullKey + ' (Array) - ' + value.length + ' Elemente'; + arrayLabel.style.cssText = 'font-weight: 600; color: #2196F3; margin-bottom: 8px; display: block; font-size: 14px;'; + arrayContainer.appendChild(arrayLabel); + + // Für jedes Array-Element ein separates Eingabefeld + const arrayItemsContainer = document.createElement('div'); + arrayItemsContainer.style.cssText = 'margin-top: 4px; padding-left: 12px;'; + + value.forEach((item, index) => { + const itemKey = `${fullKey}[${index}]`; + const itemContainer = document.createElement('div'); + itemContainer.style.cssText = 'background: #2a2a2a; padding: 8px; margin: 4px 0; border-radius: 3px; border-left: 2px solid #FF9800;'; + + const itemLabel = document.createElement('label'); + itemLabel.htmlFor = `edit-${itemKey.replace(/[.\s]/g, '-')}`; + itemLabel.textContent = `Element [${index}]`; + itemLabel.style.cssText = 'font-weight: 500; color: #FF9800; margin-bottom: 4px; display: block; font-size: 13px;'; + itemContainer.appendChild(itemLabel); + + // Prüfen, ob das Array-Element selbst ein Objekt ist + if (typeof item === 'object' && item !== null) { + const innerObjContainer = document.createElement('div'); + innerObjContainer.style.cssText = 'padding-left: 12px; margin-top: 4px;'; + buildJsonForm(item, itemKey, level + 1); + const existingObjChildren = Array.from(innerObjContainer.children); + while (innerObjContainer.firstChild) innerObjContainer.removeChild(innerObjContainer.firstChild); + existingObjChildren.forEach(ch => innerObjContainer.appendChild(ch)); + itemContainer.appendChild(innerObjContainer); + } else { + const itemFieldContainer = document.createElement('div'); + itemFieldContainer.style.cssText = 'background: var(--bg-input); padding: 8px; border-radius: 3px; margin-bottom: 4px;'; + + const input = document.createElement('input'); + const type = typeof item === 'boolean' ? 'checkbox' : typeof item === 'number' ? 'number' : 'text'; + input.type = type; + input.value = item !== null && item !== undefined ? item : ''; + input.dataset.key = itemKey; + input.dataset.type = typeof item; + input.dataset.arrayIndex = index; + + if (type === 'checkbox') input.checked = item; + else input.value = String(item !== null && item !== undefined ? item : ''); + + input.style.cssText = 'width: 100%; padding: 8px; background: #222222; color: #ffffff; border: 1px solid #555; border-radius: 3px; font-family: var(--mono); font-size: 13px;'; + itemFieldContainer.appendChild(input); + itemContainer.appendChild(itemFieldContainer); + } + + arrayItemsContainer.appendChild(itemContainer); + }); + arrayContainer.appendChild(arrayItemsContainer); + formDiv.appendChild(arrayContainer); return; } + // Für primitive Werte: Standard-Eingabefeld erstellen const fieldContainer = document.createElement('div'); fieldContainer.style.cssText = 'background: var(--bg-input); padding: 8px; border-radius: 4px; border: 1px solid transparent;'; @@ -648,13 +706,14 @@ $ python web/serve.py const type = typeof value === 'boolean' ? 'checkbox' : typeof value === 'number' ? 'number' : 'text'; const input = document.createElement('input'); input.type = type; - input.value = value; + input.value = value !== null && value !== undefined ? value : ''; input.dataset.key = fullKey; input.dataset.type = typeof value; - const displayValue = typeof value === 'boolean' ? value : String(value); + if (type === 'checkbox') input.checked = value; - else input.value = displayValue; - input.style.cssText = 'width: 100%; padding: 8px; background: #222222; color: #ffffff; border: 1px solid #cccccc; border-radius: 4px; font-family: var(--mono); font-size: 13px;'; + else input.value = String(value !== null && value !== undefined ? value : ''); + + input.style.cssText = 'width: 100%; padding: 8px; background: #222222; color: #ffffff; border: 1px solid #555; border-radius: 3px; font-family: var(--mono); font-size: 13px;'; fieldContainer.appendChild(input); formDiv.appendChild(fieldContainer); }); @@ -686,26 +745,45 @@ $ python web/serve.py } function extractJsonFromForm(formDiv) { - const result = {}; + // Map: full key -> { value, type } + const inputEntries = []; const inputs = formDiv.querySelectorAll('[data-key]'); - inputs.forEach(input => { - const keyPath = input.dataset.key; - const value = extractInputValue(input); - - const keys = keyPath.split('.'); - let current = result; - - keys.forEach((key, i) => { - if (i === keys.length - 1) { - current[key] = value; - } else { - if (!current[key]) current[key] = {}; - current = current[key]; - } - }); + const key = input.dataset.key || ''; + inputEntries.push({ key, value: extractInputValue(input) }); + }); + + // Sort by key to ensure stable parent-first construction + inputEntries.sort((a,b) => a.key.localeCompare(b.key)); + + const result = {}; + inputEntries.forEach(({ key, value }) => { + const parts = key.split('.'); + let cur = result; + for (let i = 0; i < parts.length; i++) { + const part = parts[i]; + const isLast = i === parts.length - 1; + const m = part.match(/^([^\[\]]+)\[(\d+)\]$/); + if (m && !isLast) { + const base = m[1]; + const idx = Number(m[2]); + if (!cur[base]) cur[base] = []; + if (!cur[base][idx]) cur[base][idx] = {}; + cur = cur[base][idx]; + } else if (m && isLast) { + // Array leaf + const base = m[1]; + const idx = Number(m[2]); + if (!cur[base]) cur[base] = []; + cur[base][idx] = value; + } else if (isLast) { + cur[part] = value; + } else { + if (!cur[part]) cur[part] = {}; + cur = cur[part]; + } + } }); - return result; }