Revised checkbox-tree functions and related
This commit is contained in:
@@ -0,0 +1,144 @@
|
||||
import { getSortedKeys } from './sort.js';
|
||||
|
||||
export function expandFilePrefixLevel(root) {
|
||||
if (!root || typeof root !== 'object') {
|
||||
return root;
|
||||
}
|
||||
return transformNode(root);
|
||||
}
|
||||
|
||||
function transformNode(node) {
|
||||
if (!node || typeof node !== 'object' || Array.isArray(node)) {
|
||||
return 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');
|
||||
const transformedChildren = Object.fromEntries(
|
||||
childKeys.map(key => [key, transformNode(node[key])])
|
||||
);
|
||||
|
||||
if (!hasIssueArrays) {
|
||||
return transformedChildren;
|
||||
}
|
||||
|
||||
const addressed = Array.isArray(node.addressed) ? node.addressed : [];
|
||||
const known = Array.isArray(node.known) ? node.known : [];
|
||||
if (addressed.length + known.length === 0) {
|
||||
return childKeys.length > 0 ? transformedChildren : { addressed: [], known: [] };
|
||||
}
|
||||
|
||||
const groupedByPrefix = {};
|
||||
for (const [issueType, files] of [['addressed', addressed], ['known', known]]) {
|
||||
for (const fileName of files) {
|
||||
const prefix = getFilePrefix(fileName);
|
||||
groupedByPrefix[prefix] ??= { addressed: [], known: [] };
|
||||
groupedByPrefix[prefix][issueType].push(fileName);
|
||||
}
|
||||
}
|
||||
|
||||
const transformedIssueFiles = groupHotfixPrefixes(groupedByPrefix);
|
||||
return childKeys.length === 0
|
||||
? transformedIssueFiles
|
||||
: mergeTreeNodes(transformedChildren, transformedIssueFiles);
|
||||
}
|
||||
|
||||
function groupHotfixPrefixes(groupedByPrefix) {
|
||||
const byFamily = {};
|
||||
for (const prefix of Object.keys(groupedByPrefix)) {
|
||||
const family = getReleaseFamily(prefix);
|
||||
byFamily[family] ??= [];
|
||||
byFamily[family].push(prefix);
|
||||
}
|
||||
|
||||
const result = {};
|
||||
for (const [family, members] of Object.entries(byFamily)) {
|
||||
const hasHotfixes = members.length > 1 || members.some(member => member !== family);
|
||||
if (!hasHotfixes && members[0] === family) {
|
||||
result[family] = groupedByPrefix[family];
|
||||
} else {
|
||||
result[family] = Object.fromEntries(
|
||||
members.map(member => [member, groupedByPrefix[member]])
|
||||
);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function getReleaseFamily(prefix) {
|
||||
return String(prefix || '').replace(/-h\d+$/i, '');
|
||||
}
|
||||
|
||||
function getFilePrefix(fileName) {
|
||||
const baseName = String(fileName || '');
|
||||
const underscoreIndex = baseName.indexOf('_');
|
||||
if (underscoreIndex <= 0) {
|
||||
return baseName.replace(/\.json$/i, '') || 'unknown';
|
||||
}
|
||||
return baseName.slice(0, underscoreIndex);
|
||||
}
|
||||
|
||||
function mergeTreeNodes(left, right) {
|
||||
if (!isObjectNode(left)) return right;
|
||||
if (!isObjectNode(right)) return left;
|
||||
const merged = { ...left };
|
||||
|
||||
for (const [key, rightValue] of Object.entries(right)) {
|
||||
const leftValue = merged[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]));
|
||||
} else if (isObjectNode(leftValue) && isObjectNode(rightValue)) {
|
||||
merged[key] = mergeTreeNodes(leftValue, rightValue);
|
||||
} else {
|
||||
merged[key] = rightValue;
|
||||
}
|
||||
}
|
||||
return merged;
|
||||
}
|
||||
|
||||
function isObjectNode(value) {
|
||||
return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
|
||||
}
|
||||
|
||||
export function toTreeNodes(value, path = []) {
|
||||
return getSortedKeys(value)
|
||||
.filter(key => key !== 'addressed' && key !== 'known')
|
||||
.map(key => {
|
||||
const childValue = value[key];
|
||||
const childPath = [...path, key];
|
||||
const isObject = childValue && typeof childValue === 'object';
|
||||
const hasFiles = isObject && ('addressed' in childValue || 'known' in childValue);
|
||||
const children = isObject ? toTreeNodes(childValue, childPath) : [];
|
||||
return {
|
||||
id: JSON.stringify(childPath),
|
||||
label: key,
|
||||
selectable: hasFiles || children.length > 0,
|
||||
metadata: {
|
||||
path: childPath,
|
||||
addressed: hasFiles && Array.isArray(childValue.addressed) ? childValue.addressed : [],
|
||||
known: hasFiles && Array.isArray(childValue.known) ? childValue.known : []
|
||||
},
|
||||
...(children.length > 0 ? { children } : {})
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
export function enumerateTreePaths(nodes) {
|
||||
const paths = [];
|
||||
function visit(children, parentPath = '') {
|
||||
for (const node of children) {
|
||||
const path = parentPath ? `${parentPath}>${node.id}` : node.id;
|
||||
paths.push(path);
|
||||
visit(node.children ?? [], path);
|
||||
}
|
||||
}
|
||||
visit(nodes);
|
||||
return paths;
|
||||
}
|
||||
|
||||
export function buildProductTree(products) {
|
||||
return toTreeNodes(expandFilePrefixLevel(products));
|
||||
}
|
||||
+1
-148
@@ -1,6 +1,7 @@
|
||||
import { loadIssuesForCheckedPaths } from './issues.js';
|
||||
import { getCheckedFileRefs, initializeTree, renderProductTree, restoreTreeState } from './tree.js';
|
||||
import { applyIssueSearchFilter, getIssueTypeFilters, initializeUI } from './ui.js';
|
||||
import { expandFilePrefixLevel } from './product-tree-model.js';
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
initializeUI({ onSelectionChange: refreshIssuesForCurrentSelection });
|
||||
@@ -29,151 +30,3 @@ function loadProductTree() {
|
||||
})
|
||||
.catch(error => console.error('Error loading products:', error));
|
||||
}
|
||||
|
||||
function expandFilePrefixLevel(root) {
|
||||
if (!root || typeof root !== 'object') {
|
||||
return root;
|
||||
}
|
||||
|
||||
return transformNode(root);
|
||||
}
|
||||
|
||||
function transformNode(node) {
|
||||
if (!node || typeof node !== 'object' || Array.isArray(node)) {
|
||||
return 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');
|
||||
|
||||
const transformedChildren = {};
|
||||
childKeys.forEach(key => {
|
||||
transformedChildren[key] = transformNode(node[key]);
|
||||
});
|
||||
|
||||
if (!hasIssueArrays) {
|
||||
return transformedChildren;
|
||||
}
|
||||
|
||||
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);
|
||||
});
|
||||
|
||||
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) {
|
||||
const byFamily = {};
|
||||
|
||||
Object.keys(groupedByPrefix).forEach(prefix => {
|
||||
const family = getReleaseFamily(prefix);
|
||||
if (!byFamily[family]) {
|
||||
byFamily[family] = [];
|
||||
}
|
||||
byFamily[family].push(prefix);
|
||||
});
|
||||
|
||||
const result = {};
|
||||
|
||||
Object.keys(byFamily).forEach(family => {
|
||||
const members = byFamily[family];
|
||||
const hasHotfixes = members.length > 1 || members.some(member => member !== family);
|
||||
|
||||
if (!hasHotfixes && members[0] === family) {
|
||||
result[family] = groupedByPrefix[family];
|
||||
return;
|
||||
}
|
||||
|
||||
const branch = {};
|
||||
members.forEach(member => {
|
||||
branch[member] = groupedByPrefix[member];
|
||||
});
|
||||
result[family] = branch;
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
function getReleaseFamily(prefix) {
|
||||
return String(prefix || '').replace(/-h\d+$/i, '');
|
||||
}
|
||||
|
||||
function getFilePrefix(fileName) {
|
||||
const baseName = String(fileName || '');
|
||||
const underscoreIndex = baseName.indexOf('_');
|
||||
if (underscoreIndex <= 0) {
|
||||
return baseName.replace(/\.json$/i, '') || 'unknown';
|
||||
}
|
||||
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);
|
||||
}
|
||||
|
||||
+2
-25
@@ -1,5 +1,5 @@
|
||||
import { CheckboxTree, loadManifest } from '../vendor/checkbox-tree/checkbox-tree.js';
|
||||
import { getSortedKeys } from './sort.js';
|
||||
import { toTreeNodes } from './product-tree-model.js';
|
||||
|
||||
const STORAGE_KEY = 'tree-state-v2';
|
||||
let productTree = null;
|
||||
@@ -14,6 +14,7 @@ export function initializeTree(containerId, onSelectionChange) {
|
||||
productTree = new CheckboxTree(container, {
|
||||
storageKey: STORAGE_KEY,
|
||||
initiallyCollapsed: true,
|
||||
stateEncoding: 'tiered',
|
||||
onSelectionChange: event => {
|
||||
onSelectionChange(event);
|
||||
pushTreeState();
|
||||
@@ -82,30 +83,6 @@ function pushTreeState() {
|
||||
}
|
||||
}
|
||||
|
||||
function toTreeNodes(value, path = []) {
|
||||
return getSortedKeys(value)
|
||||
.filter(key => key !== 'addressed' && key !== 'known')
|
||||
.map(key => {
|
||||
const childValue = value[key];
|
||||
const childPath = [...path, key];
|
||||
const isObject = childValue && typeof childValue === 'object';
|
||||
const hasFiles = isObject && ('addressed' in childValue || 'known' in childValue);
|
||||
const children = isObject ? toTreeNodes(childValue, childPath) : [];
|
||||
|
||||
return {
|
||||
id: JSON.stringify(childPath),
|
||||
label: key,
|
||||
selectable: hasFiles || children.length > 0,
|
||||
metadata: {
|
||||
path: childPath,
|
||||
addressed: hasFiles && Array.isArray(childValue.addressed) ? childValue.addressed : [],
|
||||
known: hasFiles && Array.isArray(childValue.known) ? childValue.known : []
|
||||
},
|
||||
...(children.length > 0 ? { children } : {})
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function dedupeFileRefs(fileRefs) {
|
||||
const seen = new Set();
|
||||
return fileRefs.filter(({ productKey, fileName }) => {
|
||||
|
||||
Reference in New Issue
Block a user