diff --git a/web/js/script.js b/web/js/script.js index 716ffdf..283f265 100644 --- a/web/js/script.js +++ b/web/js/script.js @@ -45,45 +45,56 @@ function transformNode(node) { const hasIssueArrays = Object.prototype.hasOwnProperty.call(node, 'addressed') || Object.prototype.hasOwnProperty.call(node, 'known'); + const childKeys = Object.keys(node).filter(key => key !== 'addressed' && key !== 'known'); - if (hasIssueArrays) { - const addressed = Array.isArray(node.addressed) ? node.addressed : []; - const known = Array.isArray(node.known) ? node.known : []; - const allFiles = [...addressed, ...known]; + const transformedChildren = {}; + childKeys.forEach(key => { + transformedChildren[key] = transformNode(node[key]); + }); - if (allFiles.length === 0) { - return { - addressed: [], - known: [] - }; - } - - const groupedByPrefix = {}; - - addressed.forEach(fileName => { - const prefix = getFilePrefix(fileName); - if (!groupedByPrefix[prefix]) { - groupedByPrefix[prefix] = { addressed: [], known: [] }; - } - groupedByPrefix[prefix].addressed.push(fileName); - }); - - known.forEach(fileName => { - const prefix = getFilePrefix(fileName); - if (!groupedByPrefix[prefix]) { - groupedByPrefix[prefix] = { addressed: [], known: [] }; - } - groupedByPrefix[prefix].known.push(fileName); - }); - - return groupHotfixPrefixes(groupedByPrefix); + if (!hasIssueArrays) { + return transformedChildren; } - const transformed = {}; - Object.keys(node).forEach(key => { - transformed[key] = transformNode(node[key]); + const addressed = Array.isArray(node.addressed) ? node.addressed : []; + const known = Array.isArray(node.known) ? node.known : []; + const allFiles = [...addressed, ...known]; + + if (allFiles.length === 0) { + if (childKeys.length > 0) { + return transformedChildren; + } + + return { + addressed: [], + known: [] + }; + } + + const groupedByPrefix = {}; + + addressed.forEach(fileName => { + const prefix = getFilePrefix(fileName); + if (!groupedByPrefix[prefix]) { + groupedByPrefix[prefix] = { addressed: [], known: [] }; + } + groupedByPrefix[prefix].addressed.push(fileName); }); - return transformed; + + known.forEach(fileName => { + const prefix = getFilePrefix(fileName); + if (!groupedByPrefix[prefix]) { + groupedByPrefix[prefix] = { addressed: [], known: [] }; + } + groupedByPrefix[prefix].known.push(fileName); + }); + + const transformedIssueFiles = groupHotfixPrefixes(groupedByPrefix); + if (childKeys.length === 0) { + return transformedIssueFiles; + } + + return mergeTreeNodes(transformedChildren, transformedIssueFiles); } function groupHotfixPrefixes(groupedByPrefix) { @@ -130,3 +141,39 @@ function getFilePrefix(fileName) { } return baseName.slice(0, underscoreIndex); } + +function mergeTreeNodes(left, right) { + if (!isObjectNode(left)) { + return right; + } + if (!isObjectNode(right)) { + return left; + } + + const merged = { ...left }; + + Object.keys(right).forEach(key => { + const leftValue = merged[key]; + const rightValue = right[key]; + + if (key === 'addressed' || key === 'known') { + const leftFiles = Array.isArray(leftValue) ? leftValue : []; + const rightFiles = Array.isArray(rightValue) ? rightValue : []; + merged[key] = Array.from(new Set([...leftFiles, ...rightFiles])); + return; + } + + if (isObjectNode(leftValue) && isObjectNode(rightValue)) { + merged[key] = mergeTreeNodes(leftValue, rightValue); + return; + } + + merged[key] = rightValue; + }); + + return merged; +} + +function isObjectNode(value) { + return Boolean(value) && typeof value === 'object' && !Array.isArray(value); +} diff --git a/web/js/tree.js b/web/js/tree.js index 7b60074..97d4fce 100644 --- a/web/js/tree.js +++ b/web/js/tree.js @@ -84,8 +84,8 @@ export function getCheckedFileRefs() { }); return { - addressedFiles, - knownFiles + addressedFiles: dedupeFileRefs(addressedFiles), + knownFiles: dedupeFileRefs(knownFiles) }; } @@ -93,8 +93,12 @@ function createTreeNode(name, value, path) { const container = document.createElement('div'); container.className = 'tree-item'; - const isLeafWithFiles = value && typeof value === 'object' && ('addressed' in value || 'known' in value); - const hasChildren = value && typeof value === 'object' && !isLeafWithFiles && Object.keys(value).length > 0; + const isObjectValue = value && typeof value === 'object'; + const hasFiles = isObjectValue && ('addressed' in value || 'known' in value); + const childKeys = isObjectValue + ? getSortedKeys(value).filter(key => key !== 'addressed' && key !== 'known') + : []; + const hasChildren = childKeys.length > 0; if (hasChildren) { container.classList.add('parent'); @@ -105,14 +109,14 @@ function createTreeNode(name, value, path) { toggle.style.cursor = 'pointer'; const label = document.createElement('label'); - const checkbox = createCheckbox(path); + const checkbox = createCheckbox(path, hasFiles ? value : undefined); label.appendChild(checkbox); label.appendChild(document.createTextNode(name)); const childrenContainer = document.createElement('div'); childrenContainer.className = 'tree-children'; - getSortedKeys(value).forEach(childKey => { + childKeys.forEach(childKey => { const childNode = createTreeNode(childKey, value[childKey], [...path, childKey]); childrenContainer.appendChild(childNode); }); @@ -136,7 +140,7 @@ function createTreeNode(name, value, path) { } const label = document.createElement('label'); - if (isLeafWithFiles) { + if (hasFiles) { const checkbox = createCheckbox(path, value); label.appendChild(checkbox); } @@ -151,7 +155,7 @@ function shouldStartCollapsed(name, value) { return false; } - const childKeys = Object.keys(value); + const childKeys = Object.keys(value).filter(key => key !== 'addressed' && key !== 'known'); if (childKeys.length === 0) { return false; } @@ -164,6 +168,25 @@ function shouldStartCollapsed(name, value) { return hotfixChildren.length > 0; } +function dedupeFileRefs(fileRefs) { + const seen = new Set(); + return fileRefs.filter(ref => { + const productKey = String(ref && ref.productKey ? ref.productKey : ''); + const fileName = String(ref && ref.fileName ? ref.fileName : ''); + if (!productKey || !fileName) { + return false; + } + + const key = `${productKey}::${fileName}`; + if (seen.has(key)) { + return false; + } + + seen.add(key); + return true; + }); +} + function createCheckbox(path, value) { const checkbox = document.createElement('input'); checkbox.type = 'checkbox';