diff --git a/content-editor.js b/content-editor.js index 1bd4d7d..9db5427 100644 --- a/content-editor.js +++ b/content-editor.js @@ -298,21 +298,23 @@ function makeObjCard(obj, idx, path) { function reindexItems(itemsEl) { if (!itemsEl) return; const path = itemsEl.dataset.arrayItems; + // WHY: rewrite only the index that directly follows THIS array's own path prefix. + // A generic "replace first [n]" rule corrupts nested arrays (e.g. deleting from + // services[1].specs.items rewrites the OUTER services index and scatters paths + // across services[0..n], producing sparse arrays and schema errors). + const prefix = path + '['; Array.from(itemsEl.children).forEach((child, i) => { - // Update all data-path in this child child.querySelectorAll('[data-path]').forEach(el => { const old = el.dataset.path; - // Replace the array index part: path[old_i] → path[new_i] - el.dataset.path = old.replace(/^(.+?)\[(\d+)\]/, (_,p) => p + '[' + i + ']'); + if (typeof old !== 'string' || !old.startsWith(prefix)) return; + const rest = old.slice(prefix.length); + const bracketEnd = rest.indexOf(']'); + const suffix = bracketEnd === -1 ? '' : rest.slice(bracketEnd); + el.dataset.path = prefix + i + suffix; }); // Update card header const hdr = child.querySelector('.card-header'); if (hdr) hdr.textContent = path + '[' + i + ']'; - // Update str-item textarea - const ta = child.querySelector('textarea[data-path]'); - if (ta && !ta.closest('.obj-card')) { - ta.dataset.path = path + '[' + i + ']'; - } }); } diff --git a/scripts/test-content-editor-serializer.js b/scripts/test-content-editor-serializer.js index 0f04c42..e5276b1 100644 --- a/scripts/test-content-editor-serializer.js +++ b/scripts/test-content-editor-serializer.js @@ -46,7 +46,7 @@ const document = { }; const browserContext = { document, console, setTimeout, fetch: async () => ({ json: async () => ({ ok: true }) }) }; vm.createContext(browserContext); -new vm.Script(`${browserSource}\nglobalThis.__collect = collect;`).runInContext(browserContext); +new vm.Script(`${browserSource}\nglobalThis.__collect = collect; globalThis.__reindexItems = reindexItems;`).runInContext(browserContext); assert.deepStrictEqual(JSON.parse(JSON.stringify(browserContext.__collect())), { title: 'Módosított', @@ -54,4 +54,51 @@ assert.deepStrictEqual(JSON.parse(JSON.stringify(browserContext.__collect())), { limit: 99, sections: [{ id: 'first', items: ['egy', 'kettő'], settings: { visible: false, weight: 3 } }], }); + +// Regression (MITHOME-30): reindexing a NESTED string array (e.g. services[1].specs.items +// after deleting its first item) must rewrite only the item's own trailing index. +// The old "replace first [n]" logic rewrote the OUTER array index and scattered the +// paths across services[0..n], producing sparse arrays and validation errors like +// "$.details.services[1].specs.items[0]: string érték szükséges". +const nestedEls = [1, 2, 3, 4, 5, 6].map(n => ({ dataset: { path: `details.services[1].specs.items[${n}]` } })); +const nestedItems = { + dataset: { arrayItems: 'details.services[1].specs.items' }, + children: nestedEls.map(el => ({ + querySelectorAll: selector => (selector === '[data-path]' ? [el] : []), + querySelector: () => null, + })), +}; +browserContext.__reindexItems(nestedItems); +assert.deepEqual(nestedEls.map(el => el.dataset.path), [ + 'details.services[1].specs.items[0]', + 'details.services[1].specs.items[1]', + 'details.services[1].specs.items[2]', + 'details.services[1].specs.items[3]', + 'details.services[1].specs.items[4]', + 'details.services[1].specs.items[5]', +]); + +// Reindexing the OUTER services array rewrites only the outer index and keeps +// nested field paths (including nested array item indices) intact. +const cardEls = [ + { dataset: { path: 'details.services[2].icon' } }, + { dataset: { path: 'details.services[2].specs.title' } }, + { dataset: { path: 'details.services[2].specs.items[4]' } }, +]; +const firstCardEl = { dataset: { path: 'details.services[0].icon' } }; +const servicesItems = { + dataset: { arrayItems: 'details.services' }, + children: [ + { querySelectorAll: selector => (selector === '[data-path]' ? [firstCardEl] : []), querySelector: () => null }, + { querySelectorAll: selector => (selector === '[data-path]' ? cardEls : []), querySelector: () => null }, + ], +}; +browserContext.__reindexItems(servicesItems); +assert.equal(firstCardEl.dataset.path, 'details.services[0].icon'); +assert.deepEqual(cardEls.map(el => el.dataset.path), [ + 'details.services[1].icon', + 'details.services[1].specs.title', + 'details.services[1].specs.items[4]', +]); + console.log('Content Editor serializer regression test: OK');