diff --git a/docs/changelog/assets/css/app.css b/docs/changelog/assets/css/app.css index 796ec2d6a..31de425f3 100644 --- a/docs/changelog/assets/css/app.css +++ b/docs/changelog/assets/css/app.css @@ -1,282 +1,476 @@ +/* + * CSS Variables for consistent theming + * + * Usage in CSS: background-color: var(--color-webex-blue); + * Usage in JS: element.style.backgroundColor = 'var(--color-success)'; + * + * Available variables: + * --color-success: Success state (green) + * --color-success-hover: Success hover state + * --color-danger: Error/danger state (red) + * --color-danger-hover: Danger hover state + * --color-error-bg: Error message background (light red) + * --color-error-text: Error message text color + * --color-webex-blue: Primary brand color + * --color-webex-blue-hover: Primary brand hover color + * --color-background: Page background color (light gray) + * + * Units: Use rem for font-size, padding, margin, gap, border-radius (scales with root). + * Use px for 1px borders, box-shadow blur/spread, sticky offsets, scrollbar size (crisp, consistent). + */ +:root { + --color-success: #28a745; + --color-success-hover: #218838; + --color-danger: #dc3545; + --color-danger-hover: #c82333; + --color-error-bg: #fee; + --color-error-text: #721c24; + --color-webex-blue: #00bceb; + --color-webex-blue-hover: #0099ba; + --color-background: #f5f5f5; +} + * { - box-sizing: border-box; + box-sizing: border-box; } ::-webkit-scrollbar { - -webkit-appearance: none; - width: 7px; + -webkit-appearance: none; + width: 7px; } ::-webkit-scrollbar-thumb { - border-radius: 4px; - background-color: rgba(0, 0, 0, .5); - box-shadow: 0 0 1px rgba(255, 255, 255, .5); + border-radius: 4px; + background-color: rgba(0, 0, 0, 0.5); + box-shadow: 0 0 0 1px rgba(255, 255, 255, 0.5); } body { - font-family: 'Roboto', sans-serif; - background-color: #f5f5f5; - margin: 0; - display: flex; - flex-direction: column; - min-height: 100vh; + font-family: 'Roboto', sans-serif; + background-color: var(--color-background); + margin: 0; + display: flex; + flex-direction: column; + min-height: 100vh; } -.header_anchor{ - text-decoration: none; +.header_anchor { + text-decoration: none; } #header { - position: sticky; - top: 0; - background-color: #00bceb; /* Webex blue */ - color: white; - padding: 20px; - margin: 0; - text-align: left; /* Align the title text to the left */ - box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2); - font-size: 1.5rem; - z-index: 1000; /* Ensure the header is above other content */ + position: sticky; + top: 0; + background-color: var(--color-webex-blue); + color: white; + padding: 1.25rem; /* 20px */ + margin: 0; + text-align: left; /* Align the title text to the left */ + box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2); + font-size: 1.5rem; + z-index: 1000; /* Ensure the header is above other content */ } #body { - flex: 1; - padding: 20px; + flex: 1; + padding: 1.25rem; /* 20px */ } #footer { - background-color: #f5f5f5; - text-align: center; - padding: 10px; - box-shadow: 0 -2px 5px rgba(0, 0, 0, 0.2); + background-color: var(--color-background); + text-align: center; + padding: 0.625rem; /* 10px */ + box-shadow: 0 -2px 5px rgba(0, 0, 0, 0.2); } #footer a { - color: #00bceb; /* Webex blue */ - text-decoration: none; - font-weight: bold; + color: var(--color-webex-blue); + text-decoration: none; + font-weight: bold; } #footer a:hover { - text-decoration: underline; + text-decoration: underline; } /* Rest of the CSS for form and results remains the same */ form#search-form { - padding: 10px; + padding: 0.625rem; /* 10px */ } label { - display: block; - margin-bottom: 8px; - color: #333; + display: block; + margin-bottom: 0.5rem; /* 8px */ + color: #333; } -input[type="text"], +input[type='text'], select { - width: 100%; - padding: 10px; - margin-bottom: 20px; - border: 1px solid #ccc; - border-radius: 4px; - box-sizing: border-box; + width: 100%; + padding: 0.625rem; + margin-bottom: 1.25rem; + border: 1px solid #ccc; + border-radius: 0.25rem; + box-sizing: border-box; } button { - background-color: #00bceb; /* Webex blue */ - color: white; - padding: 10px 20px; - border: none; - border-radius: 4px; - cursor: pointer; - font-size: 16px; - transition: background-color 0.3s; + background-color: var(--color-webex-blue); + color: white; + padding: 0.625rem 1.25rem; /* 10px 20px */ + border: none; + border-radius: 0.25rem; /* 4px */ + cursor: pointer; + font-size: 1rem; /* 16px */ + transition: background-color 0.3s; } button:hover { - background-color: #0099ba; /* Darker Webex blue */ + background-color: var(--color-webex-blue-hover); } table { - width: 100%; - border-collapse: collapse; + width: 100%; + border-collapse: collapse; } -table, th, td { - border: 1px solid #ccc; +table, +th, +td { + border: 1px solid #ccc; } -th, td { - padding: 10px; - text-align: left; +th, +td { + padding: 0.625rem; /* 10px */ + text-align: left; } th { - background-color: #f5f5f5; + background-color: var(--color-background); } .search-container { - background: white; - border-radius: 8px; - box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); - margin: auto; - width: 100%; - padding: 20px; /* Add padding for spacing */ - box-sizing: border-box; /* Include padding in the width calculation */ + background: white; + border-radius: 0.5rem; /* 8px */ + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); + margin: auto; + width: 100%; + padding: 1.25rem; /* 20px */ + box-sizing: border-box; /* Include padding in the width calculation */ } .form-row { - display: flex; - flex-wrap: wrap; - margin-bottom: 20px; + display: flex; + flex-wrap: wrap; + margin-bottom: 1.25rem; /* 20px */ } .form-group { - flex: 1 1 25%; /* Each form group will take up half the width of the form-row */ - padding: 0 10px; - box-sizing: border-box; + flex: 1 1 25%; /* Each form group will take up half the width of the form-row */ + padding: 0 0.625rem; /* 10px */ + box-sizing: border-box; } .full-width { - margin: 0 10px; /* Add margin to maintain spacing */ + margin: 0 0.625rem; /* 10px */ } .results-container { - width: 100%; - background: white; - padding: 20px; - border-radius: 8px; - box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); - margin: auto; - margin-top: 20px; + width: 100%; + background: white; + padding: 1.25rem; /* 20px */ + border-radius: 0.5rem; /* 8px */ + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); + margin: auto; + margin-top: 1.25rem; /* 20px */ } /* Responsive styles */ -@media (max-width: 768px) { - .form-group { - flex: 1 1 100%; /* Each form group will take up the full width on smaller screens */ - padding: 0 10px; /* Adjust padding for smaller screens */ - } +@media (max-width: 48rem) { + /* 768px */ + .form-group { + flex: 1 1 100%; /* Each form group will take up the full width on smaller screens */ + padding: 0 0.625rem; /* 10px */ + } } .changelog-item { - padding-bottom: 20px; - margin-bottom: 20px; /* Space between changelog items */ - border-bottom: 3px solid #e4e5e6; + padding-bottom: 1.25rem; /* 20px */ + margin-bottom: 1.25rem; /* 20px */ + border-bottom: 0.1875rem solid #e4e5e6; /* 3px */ } -.changelog-item h2, .changelog-item h3 { - color: #333; - margin-bottom: 10px; +.changelog-item h2, +.changelog-item h3 { + color: #333; + margin-bottom: 0.625rem; /* 10px */ } -.changelog-item .commits{ - margin-top: 20px; - margin-bottom: 20px; +.changelog-item .commits { + margin-top: 1.25rem; /* 20px */ + margin-bottom: 1.25rem; /* 20px */ } .changelog-item .commits ul { - margin-top: 10px; - list-style-type: none; - padding: 0; - max-height: 200px; - overflow: scroll; + margin-top: 0.625rem; /* 10px */ + list-style-type: none; + padding: 0; + max-height: 12.5rem; /* 200px */ + overflow: scroll; } .changelog-item .commits ul li { - background-color: #eee; - margin-bottom: 5px; - padding: 10px; - border-radius: 3px; - word-wrap: break-word; + background-color: #eee; + margin-bottom: 0.3125rem; /* 5px */ + padding: 0.625rem; /* 10px */ + border-radius: 0.1875rem; /* 3px */ + word-wrap: break-word; } .changelog-item .commit-hash { - font-family: monospace; + font-family: monospace; } -.changelog-item .related-packages{ - position: relative; +.changelog-item .related-packages { + position: relative; } -.changelog-item .related-packages .table-wrapper{ - max-height: 200px; - overflow: scroll; - border: 1px solid #ddd; - margin-top: 10px; +.changelog-item .related-packages .table-wrapper { + max-height: 12.5rem; + overflow: scroll; + border: 1px solid #ddd; + margin-top: 0.625rem; } .changelog-item .related-packages table { - width: 100%; - border-collapse: collapse; + width: 100%; + border-collapse: collapse; } - -.changelog-item table thead{ - position: sticky; - top: -1px; +.changelog-item table thead { + position: sticky; + top: -1px; } -.changelog-item th, .changelog-item td { - border: 1px solid #ddd; +.changelog-item th, +.changelog-item td { + border: 1px solid #ddd; } -.changelog-item th, .changelog-item td { - padding: 10px; - text-align: left; +.changelog-item th, +.changelog-item td { + padding: 0.625rem; /* 10px */ + text-align: left; } .changelog-item th { - background-color: #f7f7f7; + background-color: #f7f7f7; } .changelog-item tbody tr:nth-child(odd) { - background-color: #f9f9f9; + background-color: #f9f9f9; } .changelog-item tbody tr:hover { - background-color: #f1f1f1; + background-color: #f1f1f1; } .copy-button { - display: inline; - position: absolute; - margin-left: 5px; - cursor: pointer; - right: 0px; + display: inline; + position: absolute; + margin-left: 0.3125rem; /* 5px */ + cursor: pointer; + right: 0; } .copy-button img { - width: 20px; - height: auto; - margin-right: 5px; + width: 1.25rem; /* 20px */ + height: auto; + margin-right: 0.3125rem; /* 5px */ } footer { - margin: 20px 0px; + margin: 1.25rem 0; /* 20px 0px */ } footer .copyright { - text-align: center; - margin: 0 auto; + text-align: center; + margin: 0 auto; } .alert-info { - background-color: #f0f9ff; - border-left: 6px solid #00bceb; - color: #333; - padding: 10px; - margin-bottom: 20px; + background-color: #f0f9ff; + border-left: 0.375rem solid var(--color-webex-blue); /* 6px */ + color: #333; + padding: 0.625rem; /* 10px */ + margin-bottom: 1.25rem; /* 20px */ } .alert-info p.note { - font-weight: bold; - color: #00bceb; + font-weight: bold; + color: var(--color-webex-blue); } .alert-info p.warning { - font-weight: bold; - color: #ff7f0e; - display: inline; -} \ No newline at end of file + font-weight: bold; + color: #ff7f0e; + display: inline; +} + +/* ============================================ + VERSION COMPARISON STYLES + ============================================ */ + +/* Mode Toggle Buttons */ +.mode-toggle { + display: flex; + gap: 0.625rem; /* 10px */ + margin-bottom: 1.25rem; /* 20px */ +} + +.mode-toggle button { + flex: 1; + padding: 0.75rem 1.25rem; + border: 2px solid var(--color-webex-blue); + background-color: white; + color: var(--color-webex-blue); + cursor: pointer; + transition: all 0.3s; + border-radius: 0.25rem; /* 4px */ + font-weight: 500; +} + +.mode-toggle button:hover { + background-color: #e6f7fb; +} + +.mode-toggle button.active { + background-color: var(--color-webex-blue); + color: white; +} + +/* Hide utility class */ +.hide { + display: none !important; +} + +/* Comparison Form */ +#comparison-form { + margin-top: 1.25rem; /* 20px */ +} + +/* Comparison Results Table */ +#comparison-results .table-wrapper { + max-height: 31.25rem; + overflow-y: auto; + overflow-x: auto; + border: 1px solid #ddd; + border-radius: 0.25rem; + margin-top: 0.625rem; + position: relative; +} + +.comparison-table { + width: 100%; + border-collapse: collapse; + margin-top: 0; + font-size: 0.875rem; /* 14px */ +} + +.comparison-table th, +.comparison-table td { + padding: 0.75rem; + text-align: left; + border: 1px solid #ddd; +} + +.comparison-table th { + background-color: var(--color-webex-blue); + color: white; + font-weight: bold; + position: sticky; + top: 0; + z-index: 10; +} + +.comparison-table tbody tr { + transition: background-color 0.2s; +} + +.comparison-table tbody tr:hover { + background-color: var(--color-background) !important; +} + +/* Color Coding for Changes */ +.comparison-table tr.version-changed { + background-color: #fff3cd; /* Yellow - version changed */ + border-left: 0.25rem solid #ffc107; /* 4px */ +} + +.comparison-table tr.only-in-a { + background-color: #f8d7da; /* Red - removed in B */ + border-left: 0.25rem solid #dc3545; /* 4px */ +} + +.comparison-table tr.only-in-b { + background-color: #d4edda; /* Green - added in B */ + border-left: 0.25rem solid #28a745; /* 4px */ +} + +.comparison-table tr.unchanged { + background-color: #ffffff; /* White - no change */ +} + +/* Comparison Summary */ +.comparison-summary { + background: linear-gradient(135deg, #e7f3ff 0%, #f0f9ff 100%); + padding: 1.5625rem; /* 25px */ + border-radius: 0.5rem; /* 8px */ + margin-bottom: 1.25rem; /* 20px */ + border-left: 0.3125rem solid var(--color-webex-blue); /* 5px */ +} + +.comparison-summary h3 { + margin-top: 0; + margin-bottom: 0.9375rem; /* 15px */ + color: var(--color-webex-blue); + font-size: 1.5em; +} + +.summary-stats { + display: flex; + flex-wrap: wrap; + gap: 0.9375rem; /* 15px */ + margin-top: 0.9375rem; /* 15px */ +} + +.stat-item { + padding: 0.625rem 1.25rem; + background-color: white; + border-radius: 0.3125rem; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); + font-size: 0.875rem; +} + +.stat-item.changed { + border-left: 0.25rem solid #ffc107; /* 4px */ +} + +.stat-item.unchanged { + border-left: 0.25rem solid #6c757d; /* 4px */ +} + +.stat-item.added { + border-left: 0.25rem solid #28a745; /* 4px */ +} + +.stat-item.removed { + border-left: 0.25rem solid #dc3545; /* 4px */ +} + +.stat-item strong { + font-size: 1.2em; + color: #333; +} diff --git a/docs/changelog/assets/js/app.js b/docs/changelog/assets/js/app.js index 184b291e1..6368c66d1 100644 --- a/docs/changelog/assets/js/app.js +++ b/docs/changelog/assets/js/app.js @@ -1,7 +1,22 @@ // Global variable to store the current changelog and version paths let currentChangelog; const versionPaths = {}; -const github_base_url = 'https://github.com/webex/widgets/'; +let comparisonListenersInitialized = false; +const githubBaseUrl = 'https://github.com/webex/widgets/'; +import { + comparisonState, + extractPackagesFromVersion, + findLatestPackageVersion, + getEffectiveVersion, + getPackageVersion, + determinePackageStatus, + createPackageComparisonRow, + calculateComparisonStats, + buildPackagesList, + comparePackages, + fetchAndCompareVersions, + generatePackageComparisonData, +} from './comparison-view.js'; // DOM elements const versionSelectDropdown = document.getElementById('version-select'); @@ -17,23 +32,89 @@ const commitHashGroup = document.getElementById('commit-hash-group'); const searchForm = document.getElementById('search-form'); const searchButton = document.getElementById('search-button'); const searchResults = document.getElementById('search-results'); -searchResults.classList.add('hide'); + +// DOM elements - Comparison Mode +const comparisonResults = document.getElementById('comparison-results'); +const comparisonTemplateElement = document.getElementById('comparison-template'); +const comparisonForm = document.getElementById('comparison-form'); +const singleViewBtn = document.getElementById('single-view-btn'); +const comparisonViewBtn = document.getElementById('comparison-view-btn'); +const versionASelect = document.getElementById('version-a-select'); +const versionBSelect = document.getElementById('version-b-select'); +const comparisonPackageSelect = document.getElementById('comparison-package-select'); +const comparisonPackageRow = document.getElementById('comparison-package-row'); +const versionAPrereleaseSelect = document.getElementById('version-a-prerelease-select'); +const versionBPrereleaseSelect = document.getElementById('version-b-prerelease-select'); +const prereleaseRow = document.getElementById('comparison-prerelease-row'); +const compareButton = document.getElementById('compare-button'); +const clearComparisonButton = document.getElementById('clear-comparison-button'); +const copyComparisonLinkBtn = document.getElementById('copy-comparison-link'); +const comparisonHelper = document.getElementById('comparison-helper'); + +// DOM elements - Shared +const helperSection = document.getElementById('helper-section'); +const packageLevelSection = document.getElementById('package-level-comparison-section'); + +// Centralized UI visibility: map of view state -> which elements are visible/hidden +const uiViewElements = { + searchForm, + comparisonForm, + searchResults, + comparisonResults, + helperSection, + comparisonHelper, +}; + +const uiVisibilityStates = { + search: { + visible: ['searchForm', 'searchResults', 'helperSection'], + hidden: ['comparisonForm', 'comparisonResults', 'comparisonHelper'], + }, + comparison: { + visible: ['comparisonForm', 'comparisonHelper'], + hidden: ['searchForm', 'searchResults', 'helperSection', 'comparisonResults'], + }, +}; + +/** + * Updates visibility of main UI sections based on view state ('search' | 'comparison'). + * Replaces scattered classList add/remove logic with a single source of truth. + * @param {'search' | 'comparison'} viewState - The desired view mode + */ +function updateUIVisibility(viewState) { + const stateConfig = uiVisibilityStates[viewState]; + if (!stateConfig) { + console.warn(`Unknown view state: "${viewState}"`); + return; + } + stateConfig.hidden.forEach((key) => { + const el = uiViewElements[key]; + if (el) el.classList.add('hide'); + }); + stateConfig.visible.forEach((key) => { + const el = uiViewElements[key]; + if (el) el.classList.remove('hide'); + }); +} + +// Initialize UI to search view on load (single source of truth for visibility) +updateUIVisibility('search'); // Templates and Helpers - Handlebar const changelogItemTemplate = document.getElementById('changelog-item-template'); var changelogUI = Handlebars.compile(changelogItemTemplate.innerHTML); Handlebars.registerHelper('forIn', function (object) { let returnArray = []; - for (let prop in object) { + for (const prop in object) { returnArray.push({key: prop, value: object[prop]}); } return returnArray; }); -Handlebars.registerHelper('json', function (context, package, version) { +Handlebars.registerHelper('json', function (context, packageName, version) { const copyElem = { ...context, - [package]: version, + [packageName]: version, }; return JSON.stringify(copyElem); }); @@ -41,10 +122,10 @@ Handlebars.registerHelper('json', function (context, package, version) { Handlebars.registerHelper('github_linking', function (string, type) { switch (type) { case 'hash': - return `${string}`; + return `${string}`; case 'message': // if commit message has a pr number, replace that pr number with pr anchor link and send back the transformed commit message - return string.replace(/#(\d+)/g, `#$1`); + return string.replace(/#(\d+)/g, `#$1`); } }); @@ -55,6 +136,16 @@ Handlebars.registerHelper('convertDate', function (timestamp) { // Util Methods const populateFormFieldsFromURL = async () => { const queryParams = new URLSearchParams(window.location.search); + + // Skip single-view URL handling if comparison parameters are present + if ( + queryParams.has('compare') || + queryParams.has('compareStableA') || + (queryParams.has('versionA') && queryParams.has('versionB')) + ) { + return; // Comparison mode will handle these parameters + } + const searchParams = { stable_version: queryParams.get('stable_version'), package: queryParams.get('package'), @@ -72,12 +163,10 @@ const populateFormFieldsFromURL = async () => { }); } - if (searchParams.package) { - if (!packageNameInputDropdown.disabled) { - packageNameInputDropdown.value = searchParams.package; - packageNameInputDropdown.dispatchEvent(new Event('change')); - hasAtleastOneParam = true; - } + if (searchParams.package && !packageNameInputDropdown.disabled) { + packageNameInputDropdown.value = searchParams.package; + packageNameInputDropdown.dispatchEvent(new Event('change')); + hasAtleastOneParam = true; } if (searchParams.version) { @@ -122,7 +211,6 @@ const populateVersions = async () => { console.error('Error fetching version data:', error); } }; - const fetchChangelog = async (versionPath) => { try { const response = await fetch(versionPath); @@ -133,27 +221,39 @@ const fetchChangelog = async (versionPath) => { }; const populatePackageNames = (changelog) => { - let specialPackages = ['webex', '@webex/calling']; - let filteredPackages = Object.keys(changelog).filter((pkg) => !specialPackages.includes(pkg)); + let specialPackages = ['@webex/widgets', '@webex/cc-widgets']; + + // Get all packages that actually exist in this version's changelog + let allPackages = Object.keys(changelog); + + // Filter special packages that ACTUALLY EXIST in this version + let existingSpecialPackages = specialPackages.filter((pkg) => allPackages.includes(pkg)); + + // Get remaining packages (excluding special ones) + let otherPackages = allPackages.filter((pkg) => !specialPackages.includes(pkg)); // Sort the remaining packages alphabetically - filteredPackages.sort(); + otherPackages.sort(); - // Add 'webex' and '@webex/calling' back to the beginning of the array - // let sortedPackages = ['separator', ...specialPackages, 'separator', ...filteredPackages]; - let optionsHtml = ''; // Placeholder option + // Build the sorted list - only add separator if special packages exist + let sortedPackages; + if (existingSpecialPackages.length > 0) { + sortedPackages = ['separator', ...existingSpecialPackages, 'separator', ...otherPackages]; + } else { + // No special packages exist, just show others + sortedPackages = otherPackages; + } - // sortedPackages.forEach((packageName) => { - filteredPackages.forEach((packageName) => { + let optionsHtml = ''; + + sortedPackages.forEach((packageName) => { if (packageName === 'separator') { optionsHtml += ``; return; } optionsHtml += ``; }); - - // packageNameInputDropdown.value = 'webex'; - packageNameInputDropdown.innerHTML = optionsHtml; // Set all options at once + packageNameInputDropdown.innerHTML = optionsHtml; }; const doStableVersionChange = async ({stable_version}) => { @@ -163,6 +263,7 @@ const doStableVersionChange = async ({stable_version}) => { // Fetch the changelog and populate package names await fetchChangelog(versionPaths[stable_version]); populatePackageNames(currentChangelog); + updateFormState(); if (versionInput.value.trim() !== '') { validateVersionInput({version: versionInput.value}); @@ -203,6 +304,7 @@ const updateFormState = (formParams) => { commitHash: commitHashInput.value, }; } + const disable = { package: false, version: false, @@ -222,13 +324,14 @@ const updateFormState = (formParams) => { disable.commitMessage = false; disable.commitHash = false; } - + //If the package name is empty, disable the version input if (formParams.package === null || formParams.package.trim() === '') { disable.version = true; } else { disable.searchButton = false; } - + // If version filled → disable commit fields + // If commit fields filled → disable version input if (formParams.version && formParams.version.trim() !== '') { disable.version = false; disable.commitMessage = true; @@ -294,14 +397,14 @@ const updateFormState = (formParams) => { } } }; - -const doSearch_commit = (searchParams, drill_down) => { - let resulting_versions = new Set(), - resulting_commit_messages = new Set(), - resulting_commit_hash = new Set(), - search_results = []; - for (let package in drill_down) { - const thisPackage = drill_down[package]; +// Search changelog by commit message or hash.(A single commit can appear in multiple package versions.) +const doSearch_commit = (searchParams, drillDown) => { + let resultingVersions = new Set(), + resultingCommitMessages = new Set(), + resultingCommitHash = new Set(), + searchResults = []; + for (let packageName in drillDown) { + const thisPackage = drillDown[packageName]; for (let version in thisPackage) { const thisVersion = thisPackage[version]; let allHashes = new Set(), @@ -309,15 +412,15 @@ const doSearch_commit = (searchParams, drill_down) => { for (let hash in thisVersion.commits) { const thisCommit = thisVersion.commits[hash]; if (discontinueSearch) { - resulting_versions.add(`${package}-${version}`); - resulting_commit_messages.add(thisCommit); - resulting_commit_hash.add(...allHashes); + resultingVersions.add(`${packageName}-${version}`); + resultingCommitMessages.add(thisCommit); + allHashes.forEach((h) => resultingCommitHash.add(h)); } else { allHashes.add(hash); if ( - !resulting_versions.has(`${package}-${version}`) && - !resulting_commit_messages.has(thisCommit) && - !resulting_commit_hash.has(hash) + !resultingVersions.has(`${packageName}-${version}`) && + !resultingCommitMessages.has(thisCommit) && + !resultingCommitHash.has(hash) ) { if ( (searchParams.commitMessage && @@ -326,13 +429,13 @@ const doSearch_commit = (searchParams, drill_down) => { (searchParams.commitHash && (hash.includes(searchParams.commitHash) || searchParams.commitHash.startsWith(hash))) ) { - resulting_versions.add(`${package}-${version}`); - resulting_commit_messages.add(thisCommit); - resulting_commit_hash.union(allHashes); + resultingVersions.add(`${packageName}-${version}`); + resultingCommitMessages.add(thisCommit); + allHashes.forEach((h) => resultingCommitHash.add(h)); allHashes = new Set(); discontinueSearch = true; - search_results.push({ - package, + searchResults.push({ + packageName, version, published_date: thisVersion.published_date, commits: thisVersion.commits, @@ -344,57 +447,64 @@ const doSearch_commit = (searchParams, drill_down) => { } } } - return search_results; + return searchResults; }; const doSearch = (searchParams) => { - const {package, version} = searchParams; - let drill_down = {...currentChangelog}, + const pkg = searchParams.package; + const version = searchParams.version; + let drillDown = {...currentChangelog}, shouldTransform = true, - search_results = []; - - if (package !== null && package?.trim() !== '') { - drill_down = { - [package]: drill_down[package], + results = []; + // If package selected → filter to that package + if (pkg !== null && pkg?.trim() !== '') { + drillDown = { + [pkg]: drillDown[pkg], }; } + // If version selected → filter to that version (only when package and version exist) if (version !== null && version?.trim() !== '') { - drill_down = drill_down[package][version] - ? { - [package]: { - [version]: drill_down[package][version], - }, - } - : {}; + if (pkg && drillDown[pkg] && drillDown[pkg][version]) { + drillDown = { + [pkg]: { + [version]: drillDown[pkg][version], + }, + }; + } else { + drillDown = {}; + } } else if ( (searchParams.commitMessage !== null && searchParams.commitMessage?.trim() !== '') || (searchParams.commitHash !== null && searchParams.commitHash?.trim() !== '') ) { - search_results = doSearch_commit(searchParams, drill_down); + results = doSearch_commit(searchParams, drillDown); shouldTransform = false; } if (shouldTransform) { - Object.keys(drill_down).forEach((package) => { - Object.keys(drill_down[package]).forEach((version) => { - search_results.push({ - package, - version, - published_date: drill_down[package][version].published_date, - commits: drill_down[package][version].commits, - alongWith: drill_down[package][version].alongWith, + Object.keys(drillDown).forEach((pkg) => { + const versions = drillDown[pkg]; + if (versions != null && typeof versions === 'object') { + Object.keys(versions).forEach((ver) => { + results.push({ + package: pkg, + version: ver, + published_date: versions[ver].published_date, + commits: versions[ver].commits, + alongWith: versions[ver].alongWith, + }); }); - }); + } }); } - // sort search results based on published date which will be in Unit timestamp - search_results.sort((a, b) => b.published_date - a.published_date); + // sort search results based on published date (Unix timestamp) + results.sort((a, b) => b.published_date - a.published_date); const searchResultsHtml = changelogUI({ data: { - search_results, + search_results: results, stable_version: searchParams.stable_version, }, }); @@ -439,17 +549,129 @@ searchForm.addEventListener('submit', (event) => { } // Redirect to the same page with the query string - window.history.pushState({}, 'Cisco Webex JS SDK', `${window.location.pathname}?${queryParams.toString()}`); + window.history.pushState({}, 'Cisco Webex Widgets', `${window.location.pathname}?${queryParams.toString()}`); populateVersions(); }); const copyToClipboard = (copyButton) => { - navigator.clipboard.writeText(JSON.stringify(JSON.parse(copyButton.dataset.alongWith), null, 4)); - const copyText = copyButton.querySelector('span'); - copyText.textContent = 'Copied!'; + let textToCopy; + try { + textToCopy = JSON.stringify(JSON.parse(copyButton.dataset.alongWith), null, 4); + } catch (e) { + console.error('copyToClipboard: invalid data-along-with', e); + return; + } + if (navigator.clipboard && navigator.clipboard.writeText) { + navigator.clipboard + .writeText(textToCopy) + .then(() => showCopySuccess(copyButton)) + .catch((err) => { + console.error('Clipboard API failed:', err); + fallbackCopyToClipboard(textToCopy, copyButton); + }); + } else { + fallbackCopyToClipboard(textToCopy, copyButton); + } +}; + +/** + * Copy comparison link to clipboard + * Global function that can be called from HTML or JS + */ +const copyComparisonLink = () => { + const currentURL = window.location.href; + + // Try modern clipboard API first + if (navigator.clipboard && navigator.clipboard.writeText) { + navigator.clipboard + .writeText(currentURL) + .then(() => { + showCopySuccess(copyComparisonLinkBtn); + }) + .catch((err) => { + console.error('Clipboard API failed:', err); + fallbackCopyToClipboard(currentURL, copyComparisonLinkBtn); + }); + } else { + fallbackCopyToClipboard(currentURL, copyComparisonLinkBtn); + } +}; +window.copyToClipboard = copyToClipboard; +window.copyComparisonLink = copyComparisonLink; +/** + * Show success feedback on copy button + */ +const showCopySuccess = (button) => { + if (!button) return; + + const originalText = button.innerHTML; + button.innerHTML = '✓ Link Copied!'; + button.style.backgroundColor = 'var(--color-success)'; + button.style.borderColor = 'var(--color-success)'; + + setTimeout(() => { + button.innerHTML = originalText; + button.style.backgroundColor = ''; + button.style.borderColor = ''; + }, 2000); +}; + +/** + * Fallback copy method for browsers without Clipboard API (Older browsers don't support navigator.clipboard) + */ +const fallbackCopyToClipboard = (text, button) => { + // Create temporary input element + const tempInput = document.createElement('input'); + tempInput.style.position = 'fixed'; + tempInput.style.opacity = '0'; + tempInput.value = text; + document.body.appendChild(tempInput); + + // Select and copy + tempInput.select(); + tempInput.setSelectionRange(0, 99999); // For mobile devices + + try { + const successful = document.execCommand('copy'); + if (successful) { + showCopySuccess(button); + } else { + console.error('execCommand copy failed'); + showCopyError(button); + } + } catch (err) { + console.error('Fallback copy failed:', err); + showCopyError(button); + } + + // Remove temporary input + document.body.removeChild(tempInput); +}; + +/** + * Show error feedback + */ +const showCopyError = (button) => { + if (!button) { + alert('Could not copy link. Please copy manually from the address bar.'); + return; + } + + const originalText = button.innerHTML; + button.innerHTML = 'Copy Failed'; + button.style.backgroundColor = 'var(--color-danger)'; + button.style.borderColor = 'var(--color-danger)'; + setTimeout(() => { - copyText.textContent = 'Copy'; + button.innerHTML = originalText; + button.style.backgroundColor = ''; + button.style.borderColor = ''; }, 2000); + + // Also show alert with instructions + setTimeout(() => { + alert('Could not copy link automatically.\n\nPlease copy manually from the address bar:\n' + window.location.href); + }, 100); }; window.onhashchange = () => { @@ -457,3 +679,816 @@ window.onhashchange = () => { }; populateVersions(); + +let comparisonMode = false; +/* ============================================ + UI HELPER FUNCTIONS + ============================================ */ + +/** + * Show loading state for comparison + */ +const showComparisonLoading = () => { + if (!comparisonResults) return; + comparisonResults.innerHTML = '

Loading comparison...

'; + comparisonResults.classList.remove('hide'); +}; + +/** + * Show error state for comparison + * @param {Error} error - The error object + */ +const showComparisonError = (error) => { + if (!comparisonResults) return; + + console.error('Error performing version comparison:', error); + console.error('Error stack:', error.stack); + + comparisonResults.innerHTML = `
+ Error: Failed to compare versions. ${error.message} +

Check browser console for details (F12) +
`; +}; + +/* ============================================ + DATA LAYER FUNCTIONS + ============================================ */ + +/** + * UI LAYER: Handle version comparison UI updates + * @param {string} versionA - Base version + * @param {string} versionB - Target version + */ +const performVersionComparison = async (versionA, versionB) => { + // Show loading state + showComparisonLoading(); + + try { + // Fetch and compare data (pure data logic) + const result = await fetchAndCompareVersions(versionA, versionB, versionPaths); + + // Display results (UI logic) + displayComparison(result.versionA, result.versionB, result.comparisonData); + } catch (error) { + // Handle error display (UI logic) + showComparisonError(error); + } +}; + +/** + * Display comparison results + * @param {string} versionA - Base version + * @param {string} versionB - Target version + * @param {Object} comparisonData - Comparison results + */ +const displayComparison = (versionA, versionB, comparisonData) => { + if (!comparisonResults) { + console.error('comparison-results element not found!'); + return; + } + + if (!comparisonTemplateElement) { + console.error('comparison-template element not found!'); + return; + } + + const comparisonTemplate = Handlebars.compile(comparisonTemplateElement.innerHTML); + + const templateData = { + versionA, + versionB, + ...comparisonData, + }; + + console.log('Template data:', templateData); + + try { + const html = comparisonTemplate(templateData); + console.log('Generated HTML length:', html.length); + + comparisonResults.innerHTML = html; + comparisonResults.classList.remove('hide'); + + // Update URL with comparison parameters for permalinks + updateComparisonURL(versionA, versionB); + + // Show the copy link button and helper text + if (copyComparisonLinkBtn) { + copyComparisonLinkBtn.classList.remove('hide'); + console.log('Copy link button shown'); + } else { + console.warn('Copy link button not found in DOM'); + } + if (comparisonHelper) { + comparisonHelper.classList.remove('hide'); + } + + // Scroll to results smoothly + setTimeout(() => { + comparisonResults.scrollIntoView({behavior: 'smooth', block: 'start'}); + }, 100); + + console.log('Comparison displayed successfully'); + } catch (error) { + console.error('Error rendering template:', error); + comparisonResults.innerHTML = `
Error rendering comparison: ${error.message}
`; + } +}; + +/** + * Update URL with comparison parameters for sharing/bookmarking + * @param {string} versionA - Base version + * @param {string} versionB - Target version + */ +const updateComparisonURL = (versionA, versionB) => { + const url = new URL(window.location); + + // Clear any single-view parameters + url.searchParams.delete('stable_version'); + url.searchParams.delete('package'); + url.searchParams.delete('version'); + url.searchParams.delete('commitMessage'); + url.searchParams.delete('commitHash'); + // Clear enhanced (package-level) comparison params so full comparison link is not stale + url.searchParams.delete('compareStableA'); + url.searchParams.delete('compareStableB'); + url.searchParams.delete('comparePackage'); + url.searchParams.delete('compareVersionA'); + url.searchParams.delete('compareVersionB'); + + + // Set comparison parameters + url.searchParams.set('compare', `${versionA}vs${versionB}`); + + // Update URL without reloading the page + window.history.pushState({}, '', url); +}; + +/** + * Parse and handle comparison URL parameters + * Supports formats: ?compare=3.9.0vs3.10.0 or ?versionA=3.9.0&versionB=3.10.0 + */ +const handleComparisonURLParams = async () => { + const urlParams = new URLSearchParams(window.location.search); + + let versionA = null; + let versionB = null; + + // Check for ?compare=AvB format + const compareParam = urlParams.get('compare'); + if (compareParam && compareParam.includes('vs')) { + const versions = compareParam.split('vs'); + versionA = versions[0]?.trim(); + versionB = versions[1]?.trim(); + } + + // Also support ?versionA=X&versionB=Y format + if (!versionA) versionA = urlParams.get('versionA'); + if (!versionB) versionB = urlParams.get('versionB'); + + // If comparison parameters are found, switch to comparison mode + if (versionA && versionB && versionA !== versionB) { + return {versionA, versionB, shouldCompare: true}; + } + + return {shouldCompare: false}; +}; + +/** + * Switch to comparison mode programmatically + * @param {string} versionA - Base version (optional) + * @param {string} versionB - Target version (optional) + */ +const switchToComparisonMode = (versionA = null, versionB = null) => { + // Update mode + comparisonMode = true; + + // Update button states + if (comparisonViewBtn && singleViewBtn) { + comparisonViewBtn.classList.add('active', 'btn-primary'); + comparisonViewBtn.classList.remove('btn-default'); + singleViewBtn.classList.remove('active', 'btn-primary'); + singleViewBtn.classList.add('btn-default'); + } + + // Update form visibility (centralized view state) + updateUIVisibility('comparison'); + // Hide package-level comparison section in version comparison mode + if (packageLevelSection) packageLevelSection.classList.add('hide'); + + // Populate version dropdowns + if (versionSelectDropdown && versionSelectDropdown.innerHTML) { + const options = versionSelectDropdown.innerHTML; + if (versionASelect) versionASelect.innerHTML = options; + if (versionBSelect) versionBSelect.innerHTML = options; + } + + // Set selected versions if provided + if (versionA && versionASelect) versionASelect.value = versionA; + if (versionB && versionBSelect) versionBSelect.value = versionB; +}; + +/* ============================================ + ENHANCED VERSION COMPARISON HELPERS + ============================================ */ + +/** + * Get union of packages from both versions (all packages that exist in either version) + * @param {Object} changelogA - Changelog data for version A + * @param {Object} changelogB - Changelog data for version B + * @returns {Array} - Array of all package names (union) + */ +const getUnionPackages = (changelogA, changelogB) => { + const packagesA = new Set(Object.keys(changelogA)); + const packagesB = new Set(Object.keys(changelogB)); + + // Create union of both package sets + const allPackages = new Set([...packagesA, ...packagesB]); + + // Prioritize certain packages + const specialPackages = ['@webex/widgets', '@webex/cc-widgets']; + const filtered = [...allPackages].filter((pkg) => !specialPackages.includes(pkg)); + filtered.sort(); + + return [...specialPackages.filter((pkg) => allPackages.has(pkg)), ...filtered]; +}; +/** + * UI LAYER: Compare specific package versions and render results + * @param {string} packageName - Package name + * @param {string} versionASpecific - Version A + * @param {string} versionBSpecific - Version B + * @param {Object} changelogA - Changelog A + * @param {Object} changelogB - Changelog B + */ +const compareAndRenderPackageVersions = (packageName, versionASpecific, versionBSpecific, changelogA, changelogB) => { + try { + // Generate comparison data (pure data logic from comparison-view.js) + const comparisonData = generatePackageComparisonData( + packageName, + versionASpecific, + versionBSpecific, + changelogA, + changelogB + ); + + console.log('comparisonData', comparisonData); + + // Validate DOM elements + if (!comparisonResults) { + console.error('comparison-results element not found'); + return; + } + + if (!comparisonTemplateElement) { + console.error('comparison-template not found'); + return; + } + + // Render template + const template = Handlebars.compile(comparisonTemplateElement.innerHTML); + const html = template(comparisonData); + + // Update DOM + comparisonResults.innerHTML = html; + comparisonResults.classList.remove('hide'); + + // Update URL for sharing + updateEnhancedComparisonURL( + versionASelect.value, + versionBSelect.value, + packageName, + comparisonData.versionA, + comparisonData.versionB + ); + + // Show copy link button and helper + if (copyComparisonLinkBtn) copyComparisonLinkBtn.classList.remove('hide'); + if (comparisonHelper) comparisonHelper.classList.remove('hide'); + + // Scroll to results + setTimeout(() => { + comparisonResults.scrollIntoView({behavior: 'smooth', block: 'start'}); + }, 100); + } catch (error) { + console.error('Error in package comparison:', error); + + // Show error to user + if (error.message.includes('Could not find version data')) { + alert(error.message); + } else { + showComparisonError(error); + } + } +}; +/** + * Populate the package dropdown with union of packages from both versions + * @param {Object} changelogA - Changelog for base version + * @param {Object} changelogB - Changelog for target version + */ +const populateUnionPackages = (changelogA, changelogB) => { + if (!comparisonPackageSelect || !comparisonPackageRow) return; + + const allPackages = getUnionPackages(changelogA, changelogB); + + if (allPackages.length === 0) { + comparisonPackageSelect.innerHTML = ''; + comparisonPackageRow.style.display = 'none'; + return; + } + + let optionsHtml = ''; + allPackages.forEach((pkg) => { + optionsHtml += ``; + }); + + comparisonPackageSelect.innerHTML = optionsHtml; + comparisonPackageRow.style.display = 'flex'; +}; + +/** + * Populate pre-release versions for a selected package + * @param {string} packageName - Selected package name + * @param {Object} changelog - Changelog data + * @param {string} selectId - ID of the select element to populate + * @param {string} stableVersion - The stable version (e.g., 3.3.1) + */ +const populatePrereleaseVersions = (packageName, changelog, selectId, stableVersion) => { + const versionSelect = + selectId === 'version-a-prerelease-select' ? versionAPrereleaseSelect : versionBPrereleaseSelect; + + if (!versionSelect || !packageName) { + if (versionSelect) { + versionSelect.innerHTML = ''; + versionSelect.disabled = true; + } + return; + } + + // Check if package exists in this changelog (it might not for union packages) + if (!changelog[packageName]) { + if (versionSelect) { + versionSelect.innerHTML = ''; + versionSelect.disabled = true; + } + return; + } + + // Get all versions for this package + const allVersions = Object.keys(changelog[packageName]); + + // Filter for pre-release versions matching the stable version + // e.g., for stable version 3.3.1, get 3.3.1-next.1, 3.3.1-next.22, etc. + const prereleaseVersions = allVersions.filter((v) => v.startsWith(stableVersion + '-') && v !== stableVersion); + + // Sort by version (newest first based on published date) + prereleaseVersions.sort((a, b) => { + const dateA = changelog[packageName][a]?.published_date || 0; + const dateB = changelog[packageName][b]?.published_date || 0; + return dateB - dateA; + }); + + let optionsHtml = ''; + + // Also add the stable version itself as an option + if (changelog[packageName][stableVersion]) { + const stableDate = changelog[packageName][stableVersion]?.published_date; + const dateStr = stableDate ? new Date(stableDate).toLocaleDateString() : ''; + optionsHtml += ``; + + if (prereleaseVersions.length > 0) { + optionsHtml += ``; + } + } + + // Add pre-release versions + prereleaseVersions.forEach((version) => { + const date = changelog[packageName][version]?.published_date; + const dateStr = date ? new Date(date).toLocaleDateString() : ''; + optionsHtml += ``; + }); + + versionSelect.innerHTML = optionsHtml; + versionSelect.disabled = false; +}; + +/* ============================================ + MAIN DATA LAYER FUNCTION + ============================================ */ + +/** + * Update URL with enhanced comparison parameters + */ +const updateEnhancedComparisonURL = (stableA, stableB, packageName, versionA, versionB) => { + const url = new URL(window.location); + + // Clear old parameters + url.searchParams.delete('stable_version'); + url.searchParams.delete('package'); + url.searchParams.delete('version'); + url.searchParams.delete('commitMessage'); + url.searchParams.delete('commitHash'); + url.searchParams.delete('compare'); + + // Set new comparison parameters + url.searchParams.set('compareStableA', stableA); + url.searchParams.set('compareStableB', stableB); + url.searchParams.set('comparePackage', packageName); + url.searchParams.set('compareVersionA', versionA); + url.searchParams.set('compareVersionB', versionB); + + window.history.pushState({}, '', url); +}; + +/** + * Handle URL parameters for enhanced comparison + */ +const handleEnhancedComparisonURL = async () => { + const urlParams = new URLSearchParams(window.location.search); + + const stableA = urlParams.get('compareStableA'); + const stableB = urlParams.get('compareStableB'); + const packageName = urlParams.get('comparePackage'); + const versionA = urlParams.get('compareVersionA'); + const versionB = urlParams.get('compareVersionB'); + + if (stableA && stableB && packageName && versionA && versionB) { + return {stableA, stableB, packageName, versionA, versionB, shouldCompare: true}; + } + + return {shouldCompare: false}; +}; + +/** + * Populate version dropdowns for comparison mode + */ +const populateComparisonVersions = () => { + if (versionSelectDropdown && versionSelectDropdown.innerHTML) { + const options = versionSelectDropdown.innerHTML; + if (versionASelect) versionASelect.innerHTML = options; + if (versionBSelect) versionBSelect.innerHTML = options; + } +}; + +/** + * Reset comparison form selections + */ +const resetComparisonSelections = () => { + if (comparisonPackageSelect) comparisonPackageSelect.value = ''; + if (versionAPrereleaseSelect) versionAPrereleaseSelect.value = ''; + if (versionBPrereleaseSelect) versionBPrereleaseSelect.value = ''; + if (comparisonPackageRow) comparisonPackageRow.style.display = 'none'; + if (prereleaseRow) prereleaseRow.style.display = 'none'; +}; + +/** + * Clear all comparison form inputs and state + */ +const clearComparisonForm = () => { + if (versionASelect) versionASelect.value = ''; + if (versionBSelect) versionBSelect.value = ''; + resetComparisonSelections(); + if (comparisonResults) comparisonResults.classList.add('hide'); + + comparisonState.reset(); + + if (copyComparisonLinkBtn) copyComparisonLinkBtn.classList.add('hide'); + if (comparisonHelper) comparisonHelper.classList.add('hide'); + if (compareButton) compareButton.disabled = false; +}; + +/** + * Clear comparison URL parameters + */ +const clearComparisonURLParams = () => { + const url = new URL(window.location); + [ + 'compare', + 'versionA', + 'versionB', + 'compareStableA', + 'compareStableB', + 'comparePackage', + 'compareVersionA', + 'compareVersionB', + ].forEach((param) => { + url.searchParams.delete(param); + }); + window.history.pushState({}, '', url); +}; + +/** + * Check and update comparison button state based on form selections + */ +const updateCompareButtonState = () => { + if (!compareButton) return; + + const selectedPackage = comparisonPackageSelect ? comparisonPackageSelect.value : null; + const versionASpecific = versionAPrereleaseSelect ? versionAPrereleaseSelect.value : null; + const versionBSpecific = versionBPrereleaseSelect ? versionBPrereleaseSelect.value : null; + const prereleaseRowVisible = prereleaseRow && prereleaseRow.style.display !== 'none'; + + if (selectedPackage) { + // Package selected - require at least one pre-release version + if (!prereleaseRowVisible || (!versionASpecific && !versionBSpecific)) { + compareButton.disabled = true; + } else { + compareButton.disabled = false; + } + } else { + // No package selected - enable for full version comparison + compareButton.disabled = false; + } +}; + +/** + * Update pre-release row labels with version numbers + */ +const updatePrereleaseLabels = () => { + if (!prereleaseRow) return; + + const labelA = prereleaseRow.querySelector('label[for="version-a-prerelease-select"]'); + const labelB = prereleaseRow.querySelector('label[for="version-b-prerelease-select"]'); + if (labelA) labelA.textContent = `Pre-release Version for Base (${comparisonState.currentStableA}):`; + if (labelB) labelB.textContent = `Pre-release Version for Target (${comparisonState.currentStableB}):`; +}; + +/** + * Handle stable version changes - fetch changelogs and populate packages + */ +const handleStableVersionChange = async () => { + console.log('🟢 handleStableVersionChange FIRED'); + const stableA = versionASelect.value; + const stableB = versionBSelect.value; + + resetComparisonSelections(); + updateCompareButtonState(); + + if (stableA && stableB) { + try { + const [changelogA, changelogB] = await Promise.all([ + fetch(versionPaths[stableA]).then((res) => res.json()), + fetch(versionPaths[stableB]).then((res) => res.json()), + ]); + + comparisonState.update(changelogA, changelogB, stableA, stableB); + populateUnionPackages(changelogA, changelogB); + updateCompareButtonState(); + } catch (error) { + console.error('Error loading changelogs:', error); + alert('Error loading version data. Please try again.'); + } + } +}; + +/** + * Handle package selection - populate pre-release versions + */ +const handlePackageChange = () => { + console.log('🟢 handlePackageChange FIRED'); + const selectedPackage = comparisonPackageSelect.value; + + if (versionAPrereleaseSelect) versionAPrereleaseSelect.value = ''; + if (versionBPrereleaseSelect) versionBPrereleaseSelect.value = ''; + + if (selectedPackage && comparisonState.cachedChangelogA && comparisonState.cachedChangelogB) { + populatePrereleaseVersions( + selectedPackage, + comparisonState.cachedChangelogA, + 'version-a-prerelease-select', + comparisonState.currentStableA + ); + populatePrereleaseVersions( + selectedPackage, + comparisonState.cachedChangelogB, + 'version-b-prerelease-select', + comparisonState.currentStableB + ); + + if (prereleaseRow) { + prereleaseRow.style.display = 'flex'; + updatePrereleaseLabels(); + } + } else { + if (prereleaseRow) prereleaseRow.style.display = 'none'; + } + + updateCompareButtonState(); +}; + +/** + * Switch to single view mode + */ +const switchToSingleViewMode = () => { + console.log('🔵 Switching to SINGLE VIEW mode'); + comparisonMode = false; + + // Update button styles + singleViewBtn.classList.add('active', 'btn-primary'); + singleViewBtn.classList.remove('btn-default'); + comparisonViewBtn.classList.remove('active', 'btn-primary'); + comparisonViewBtn.classList.add('btn-default'); + + // Toggle visibility (centralized view state) + updateUIVisibility('search'); + + clearComparisonURLParams(); +}; + +/** + * Switch to comparison view mode + */ +const switchToComparisonViewMode = () => { + console.log('🔵 Switching to COMPARISON VIEW mode'); + comparisonMode = true; + + // Update button styles + comparisonViewBtn.classList.add('active', 'btn-primary'); + comparisonViewBtn.classList.remove('btn-default'); + singleViewBtn.classList.remove('active', 'btn-primary'); + singleViewBtn.classList.add('btn-default'); + + // Toggle visibility (centralized view state) + updateUIVisibility('comparison'); + + populateComparisonVersions(); +}; + +/** + * Validate comparison form inputs + */ +const validateComparisonInputs = (stableA, stableB, selectedPackage, versionASpecific, versionBSpecific) => { + if (!stableA || !stableB) { + alert('Please select both stable versions'); + return false; + } + + if (selectedPackage && !versionASpecific && !versionBSpecific) { + alert('Please select at least one pre-release version, or leave package empty for full version comparison'); + return false; + } + + return true; +}; + +/** + * Handle comparison form submission + */ +const handleComparisonSubmit = (event) => { + event.preventDefault(); + + const stableA = versionASelect.value; + const stableB = versionBSelect.value; + const selectedPackage = comparisonPackageSelect?.value; + const versionASpecific = versionAPrereleaseSelect?.value; + const versionBSpecific = versionBPrereleaseSelect?.value; + + if (!validateComparisonInputs(stableA, stableB, selectedPackage, versionASpecific, versionBSpecific)) { + return; + } + + if (selectedPackage && (versionASpecific || versionBSpecific)) { + // Package-level comparison + const finalVersionA = versionASpecific || stableA; + const finalVersionB = versionBSpecific || stableB; + console.log('Comparing:', finalVersionA, 'vs', finalVersionB); + + compareAndRenderPackageVersions( + selectedPackage, + finalVersionA, + finalVersionB, + comparisonState.cachedChangelogA, + comparisonState.cachedChangelogB + ); + } else { + // Full version comparison + performVersionComparison(stableA, stableB); + } + + if (compareButton) compareButton.disabled = false; +}; + +/** + * Handle clear button click + */ +const handleClearClick = () => { + clearComparisonForm(); + clearComparisonURLParams(); +}; + +/** + * Setup event listeners for comparison mode + */ +const setupComparisonEventListeners = () => { + if (comparisonListenersInitialized) { + console.log('🔴 Comparison listeners already initialized,skipping......'); + return; + } + console.log('🟢 Setting up comparison event listeners first time......'); + comparisonListenersInitialized = true; + // Mode toggle buttons + if (singleViewBtn) singleViewBtn.addEventListener('click', switchToSingleViewMode); + + if (comparisonViewBtn) comparisonViewBtn.addEventListener('click', switchToComparisonViewMode); + + // Version and package selectors + if (versionASelect) versionASelect.addEventListener('change', handleStableVersionChange); + if (versionBSelect) versionBSelect.addEventListener('change', handleStableVersionChange); + + if (comparisonPackageSelect) comparisonPackageSelect.addEventListener('change', handlePackageChange); + + // Pre-release version selectors + if (versionAPrereleaseSelect) versionAPrereleaseSelect.addEventListener('change', updateCompareButtonState); + + if (versionBPrereleaseSelect) versionBPrereleaseSelect.addEventListener('change', updateCompareButtonState); + + // Form actions + if (comparisonForm) comparisonForm.addEventListener('submit', handleComparisonSubmit); + if (clearComparisonButton) clearComparisonButton.addEventListener('click', handleClearClick); + if (copyComparisonLinkBtn) copyComparisonLinkBtn.addEventListener('click', copyComparisonLink); + + comparisonListenersInitialized = true; + console.log('comparison listeners are initialized sucessfully......'); +}; + +/** + * Handle enhanced comparison URL parameters on page load + */ +const loadEnhancedComparisonFromURL = async (enhancedParams) => { + switchToComparisonMode(); + + await new Promise((resolve) => setTimeout(resolve, 300)); + + versionASelect.value = enhancedParams.stableA; + versionBSelect.value = enhancedParams.stableB; + await handleStableVersionChange(); + + await new Promise((resolve) => setTimeout(resolve, 300)); + + comparisonPackageSelect.value = enhancedParams.packageName; + handlePackageChange(); + + await new Promise((resolve) => setTimeout(resolve, 300)); + + versionAPrereleaseSelect.value = enhancedParams.versionA; + versionBPrereleaseSelect.value = enhancedParams.versionB; + + compareAndRenderPackageVersions( + enhancedParams.packageName, + enhancedParams.versionA, + enhancedParams.versionB, + comparisonState.cachedChangelogA, + comparisonState.cachedChangelogB + ); +}; + +/** + * Handle standard comparison URL parameters on page load + */ +const loadStandardComparisonFromURL = async (urlParams) => { + switchToComparisonMode(urlParams.versionA, urlParams.versionB); + + await new Promise((resolve) => setTimeout(resolve, 300)); + + performVersionComparison(urlParams.versionA, urlParams.versionB); +}; + +/** + * Initialize comparison mode functionality (Refactored) + */ +const initializeComparisonMode = async () => { + // Setup all event listeners + setupComparisonEventListeners(); + + // Check for URL parameters on page load + const enhancedParams = await handleEnhancedComparisonURL(); + if (enhancedParams.shouldCompare) { + await loadEnhancedComparisonFromURL(enhancedParams); + return; + } + + // Check for standard comparison URL + const urlParams = await handleComparisonURLParams(); + if (urlParams.shouldCompare) { + await loadStandardComparisonFromURL(urlParams); + } +}; + +/** + * Initialize application in correct order to prevent race conditions + * This ensures versionPaths is populated before URL parameters are checked + */ +const initializeApplication = async () => { + // Step 1: Load version paths first (critical for URL parameter handling!) + await populateVersions(); + + // Step 2: Then initialize comparison mode (which checks URL params) + await initializeComparisonMode(); +}; + +// Wait for DOM to be ready, then initialize +if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', initializeApplication); +} else { + // DOM is already ready + initializeApplication(); +} \ No newline at end of file diff --git a/docs/changelog/assets/js/comparison-view.js b/docs/changelog/assets/js/comparison-view.js new file mode 100644 index 000000000..7352606aa --- /dev/null +++ b/docs/changelog/assets/js/comparison-view.js @@ -0,0 +1,387 @@ +//Data Extraction and Processing +const comparisonState = { + cachedChangelogA: null, + cachedChangelogB: null, + currentStableA: null, + currentStableB: null, + + reset() { + this.cachedChangelogA = null; + this.cachedChangelogB = null; + this.currentStableA = null; + this.currentStableB = null; + }, + + update(changelogA, changelogB, stableA, stableB) { + this.cachedChangelogA = changelogA; + this.cachedChangelogB = changelogB; + this.currentStableA = stableA; + this.currentStableB = stableB; + }, +}; +const extractPackagesFromVersion = (changelog, specificVersions = null) => { + const packageMap = {}; + + for (const packageName of Object.keys(changelog)) { + const packageVersions = changelog[packageName]; + console.log('packageVersions', packageVersions); + + // Safety check: ensure packageVersions is an object + if (!packageVersions || typeof packageVersions !== 'object') continue; + + const versionKeys = Object.keys(packageVersions); + console.log('versionKeys', versionKeys); + + if (versionKeys.length === 0) continue; + + let selectedVersion = null; + + // Check if user specified a specific version for this package + if (specificVersions && specificVersions[packageName]) { + const requestedVersion = specificVersions[packageName]; + + if (packageVersions[requestedVersion]) { + selectedVersion = requestedVersion; + } + } + + // If no specific version requested or not found, use earliest (first) version + if (!selectedVersion) { + let earliestVersion = versionKeys[0]; + let earliestDate = packageVersions[earliestVersion]?.published_date || Infinity; + + for (const version of versionKeys) { + const publishedDate = packageVersions[version]?.published_date || Infinity; + if (publishedDate < earliestDate) { + earliestDate = publishedDate; + earliestVersion = version; + } + } + + selectedVersion = earliestVersion; + } + + packageMap[packageName] = selectedVersion; + } + + return packageMap; +}; +const findLatestPackageVersion = (changelog, packageName) => { + if (!changelog[packageName]) return null; + + const versions = Object.keys(changelog[packageName]); + if (versions.length === 0) return null; + + // Find the latest version by published date + let latestVersion = versions[0]; + let latestDate = changelog[packageName][versions[0]].published_date || 0; + + versions.forEach((ver) => { + const publishedDate = changelog[packageName][ver].published_date || 0; + if (publishedDate > latestDate) { + latestDate = publishedDate; + latestVersion = ver; + } + }); + + return latestVersion; +}; +const getEffectiveVersion = (changelog, packageName, requestedVersion) => { + // If requested version exists, use it + if (changelog[packageName]?.[requestedVersion]) { + return requestedVersion; + } + + // Otherwise, fallback to latest version + return findLatestPackageVersion(changelog, packageName); +}; +const getPackageVersion = (packageName, alongWithData, changelog) => { + // Priority 1: Check alongWith data + if (alongWithData[packageName]) { + return alongWithData[packageName]; + } + + // Priority 2: Find latest version in changelog + return findLatestPackageVersion(changelog, packageName); +}; +//Comparison Logic +const determinePackageStatus = (versionA, versionB, dataA, dataB) => { + if (!dataA && dataB) { + return {status: 'Added', changeClass: 'only-in-b'}; + } + + if (dataA && !dataB) { + return {status: 'Removed', changeClass: 'only-in-a'}; + } + + if (versionA !== versionB) { + return {status: 'Version Changed', changeClass: 'version-changed'}; + } + + return {status: 'Unchanged', changeClass: 'unchanged'}; +}; +const createPackageComparisonRow = (packageName, versionA, versionB, statusInfo) => { + return { + packageName, + versionA: versionA || 'N/A', + versionB: versionB || 'N/A', + status: statusInfo.status, + changeClass: statusInfo.changeClass, + }; +}; +const calculateComparisonStats = (packages) => { + const stats = { + changedCount: 0, + unchangedCount: 0, + onlyInACount: 0, + onlyInBCount: 0, + }; + + packages.forEach((pkg) => { + switch (pkg.status) { + case 'Version Changed': + stats.changedCount++; + break; + case 'Unchanged': + stats.unchangedCount++; + break; + case 'Removed': + stats.onlyInACount++; + break; + case 'Added': + stats.onlyInBCount++; + break; + } + }); + + return stats; +}; + +const buildPackagesList = ( + mainPackage, + effectiveVersionA, + effectiveVersionB, + pkgDataA, + pkgDataB, + changelogA, + changelogB +) => { + const packagesArray = []; + + // Add main package row + const mainStatus = determinePackageStatus(effectiveVersionA, effectiveVersionB, pkgDataA, pkgDataB); + packagesArray.push(createPackageComparisonRow(mainPackage, effectiveVersionA, effectiveVersionB, mainStatus)); + + // Get alongWith data + const alongWithA = pkgDataA?.alongWith || {}; + const alongWithB = pkgDataB?.alongWith || {}; + + // Get all packages from both changelogs + const allPackages = new Set([...Object.keys(changelogA), ...Object.keys(changelogB)]); + + // Remove main package (already added) + allPackages.delete(mainPackage); + + // Add comparison rows for all related packages + allPackages.forEach((pkg) => { + const pkgVerA = getPackageVersion(pkg, alongWithA, changelogA); + const pkgVerB = getPackageVersion(pkg, alongWithB, changelogB); + + const statusInfo = determinePackageStatus( + pkgVerA, + pkgVerB, + pkgVerA ? {} : null, // Simplified - just check if version exists + pkgVerB ? {} : null + ); + + packagesArray.push(createPackageComparisonRow(pkg, pkgVerA, pkgVerB, statusInfo)); + }); + + // Sort packages alphabetically + packagesArray.sort((a, b) => a.packageName.localeCompare(b.packageName)); + + return packagesArray; +}; +const comparePackages = (packagesA, packagesB, changelogA, changelogB, stableVersionA, stableVersionB) => { + // Get ALL package names from both changelogs (entire changelog, not just specific versions) + const allPackageNames = new Set([ + ...Object.keys(changelogA), //ALL packages in changelog A + ...Object.keys(changelogB), //ALL packages in changelog B + ]); + + const packages = []; + let changedCount = 0; + let unchangedCount = 0; + let onlyInACount = 0; + let onlyInBCount = 0; + + // Helper function to find stable version first, then highest pre-release version + const findStableVersion = (changelog, packageName, stableVersion) => { + if (!changelog[packageName]) return null; + + const versions = Object.keys(changelog[packageName]); + if (versions.length === 0) return null; + + // Escape dots in version string for regex (3.4.0 -> 3\.4\.0) + const escapedVersion = stableVersion.replace(/\./g, '\\.'); + + // Priority 1: Find exact stable version (e.g., "3.4.0" only, no suffixes) + const exactStablePattern = new RegExp(`^${escapedVersion}$`); + const exactStableVersion = versions.find((ver) => exactStablePattern.test(ver)); + + if (exactStableVersion) { + return exactStableVersion; + } + + // Priority 2: Find oldest pre-release version (any tag: next, alpha, beta, rc, etc.) + // Pattern: 3.4.0-{tag}.{number} -> captures tag and number + const prereleasePattern = new RegExp(`^${escapedVersion}-([a-z]+)\\.(\\d+)$`, 'i'); + + const prereleaseVersions = versions + .filter((ver) => prereleasePattern.test(ver)) + .sort((a, b) => { + const matchA = a.match(prereleasePattern); + const matchB = b.match(prereleasePattern); + if (!matchA || !matchB) return 0; + const numA = parseInt(matchA[2], 10); + const numB = parseInt(matchB[2], 10); + return numA - numB; // Sort ascending (lowest first) + }); + //console.log('prereleaseVersions', prereleaseVersions); + //console.log('versions', versions); + // Return highest pre-release version, or fallback to first available + return prereleaseVersions[0] || versions[0]; + }; + + allPackageNames.forEach((packageName) => { + // Use release version per stable train (exact stable or highest prerelease), not chronologically earliest + const versionA = findStableVersion(changelogA, packageName, stableVersionA); + const versionB = findStableVersion(changelogB, packageName, stableVersionB); + + let status, changeClass; //Declare variables for status label and CSS class + + if (versionA && versionB) { + //checks if package is in both changelogs + if (versionA === versionB) { + //if versionA is the same as versionB, then it is unchanged + status = 'Unchanged'; + changeClass = 'unchanged'; + unchangedCount++; + } else { + status = 'Version Changed'; + changeClass = 'version-changed'; + changedCount++; + } + } else if (versionA && !versionB) { + status = 'Removed'; + changeClass = 'only-in-a'; + onlyInACount++; + } else if (!versionA && versionB) { + status = 'Added'; + changeClass = 'only-in-b'; + onlyInBCount++; + } + + packages.push({ + packageName, + versionA: versionA || 'N/A', + versionB: versionB || 'N/A', + status, + changeClass, + }); + }); + + // Sort packages alphabetically + packages.sort((a, b) => a.packageName.localeCompare(b.packageName)); + + return { + packages, + totalPackages: allPackageNames.size, + changedCount, + unchangedCount, + onlyInACount, + onlyInBCount, + }; +}; + +//Data Fetching +const fetchAndCompareVersions = async (versionA, versionB, versionPaths) => { + const [changelogA, changelogB] = await Promise.all([ + fetch(versionPaths[versionA]).then((res) => { + if (!res.ok) throw new Error(`Failed to fetch ${versionA}`); + return res.json(); + }), + fetch(versionPaths[versionB]).then((res) => { + if (!res.ok) throw new Error(`Failed to fetch ${versionB}`); + return res.json(); + }), + ]); + + // Extract packages from both versions + const packagesA = extractPackagesFromVersion(changelogA); + const packagesB = extractPackagesFromVersion(changelogB); + + // Compare packages + const comparisonData = comparePackages(packagesA, packagesB, changelogA, changelogB, versionA, versionB); + + return { + versionA, + versionB, + comparisonData, + }; +}; +const generatePackageComparisonData = (packageName, versionASpecific, versionBSpecific, changelogA, changelogB) => { + const effectiveVersionA = getEffectiveVersion(changelogA, packageName, versionASpecific); + const effectiveVersionB = getEffectiveVersion(changelogB, packageName, versionBSpecific); + console.log('effectiveVersionA', effectiveVersionA); + console.log('effectiveVersionB', effectiveVersionB); + // Get package data from changelogs + const pkgDataA = changelogA[packageName]?.[effectiveVersionA]; + const pkgDataB = changelogB[packageName]?.[effectiveVersionB]; + console.log('pkgDataA', pkgDataA); + console.log('pkgDataB', pkgDataB); + + // Validate versions exist + if (!pkgDataA && !pkgDataB) { + throw new Error(`Could not find version data for ${packageName}`); + } + + // Build packages list including main package and all related packages + const packagesArray = buildPackagesList( + packageName, + effectiveVersionA, + effectiveVersionB, + pkgDataA, + pkgDataB, + changelogA, + changelogB + ); + + // Calculate statistics + const stats = calculateComparisonStats(packagesArray); + + // Return pure data object + return { + packageName, + versionA: effectiveVersionA, + versionB: effectiveVersionB, + packages: packagesArray, + totalPackages: packagesArray.length, + ...stats, + }; +}; +//Export All the functions +export { + comparisonState, + extractPackagesFromVersion, + findLatestPackageVersion, + getEffectiveVersion, + getPackageVersion, + determinePackageStatus, + createPackageComparisonRow, + calculateComparisonStats, + buildPackagesList, + comparePackages, + fetchAndCompareVersions, + generatePackageComparisonData, +}; diff --git a/docs/changelog/index.html b/docs/changelog/index.html index b2e9b74dc..18a7eb548 100644 --- a/docs/changelog/index.html +++ b/docs/changelog/index.html @@ -82,9 +82,19 @@ +
+
+ +
+ + +
+
+
+
@@ -124,6 +134,58 @@
+ + +
+
+
+ + +
+
+ + +
+
+ + + + + +
+
+ + + +
+
+
@@ -131,7 +193,7 @@ Search Examples:
+ + +
+ Version Comparison Examples: + + How to use version comparison: + +
+ +
+
+ + +
+ + +
^
@@ -230,6 +326,60 @@

Published: {{convertDate this.published_date}}

{{/each}} - + + + + +