diff options
author | Daniel Smith <[email protected]> | 2025-04-29 10:32:37 +0200 |
---|---|---|
committer | Daniel Smith <[email protected]> | 2025-05-20 09:50:29 +0000 |
commit | e9119a682e6ad9f70845a9c6efacc219ed5407fa (patch) | |
tree | 44ec03f82adec7e40da94c6f06c11a5d51c7d6a3 | |
parent | f3b411be465684ecb238a7561f7a5e2393f0b98d (diff) |
It may be helpful during release crunch time to
filter on the release branch target and ensure that
all changes targeting that branch get merged.
This patch adds a new Branches filter where one or more
branches can be specified. The backend will return
only those change that have a yet-unmerged or unpicked
change to the specified branches.
This patch also updates styles and organization of
the filter area to improve visual clarity.
Task-number: QTQAINFRA-7144
Change-Id: I5dd98940b9d3bfc4f26b5f37a83a1ef2e511b917
Reviewed-by: Daniel Smith <[email protected]>
-rw-r--r-- | src/main/java/com/google/gerrit/plugins/cherrypickstatus/CherryPickRestApi.java | 15 | ||||
-rw-r--r-- | src/main/resources/static/cherrypick-status-viewer.js | 383 |
2 files changed, 361 insertions, 37 deletions
diff --git a/src/main/java/com/google/gerrit/plugins/cherrypickstatus/CherryPickRestApi.java b/src/main/java/com/google/gerrit/plugins/cherrypickstatus/CherryPickRestApi.java index d559305..2e70b88 100644 --- a/src/main/java/com/google/gerrit/plugins/cherrypickstatus/CherryPickRestApi.java +++ b/src/main/java/com/google/gerrit/plugins/cherrypickstatus/CherryPickRestApi.java @@ -105,6 +105,14 @@ public class CherryPickRestApi { this.onlyExternalTqtc = onlyExternalTqtc; } + // Option for filtering by branch names. Populated from query parameters. + @Option(name = "--branches", usage = "Comma-separated list of branches to filter by") + private String branches; + + public void setBranches(String branches) { + this.branches = branches; + } + // Constructor for dependency injection. Initializes fields. @Inject public GetDashboard( @@ -208,7 +216,12 @@ public class CherryPickRestApi { requestBody.put("exclAbandon", true); } if (onlyExternalTqtc) { - requestBody.put("onlyExternalTqtc", true); + requestBody.put("onlyExternalTqtc", true); + } + if (branches != null && !branches.isEmpty()) { + // Split the comma-separated string into a list of branch names + List<String> branchList = List.of(branches.split(",")); + requestBody.put("branches", branchList); // Pass branches as a list of strings } // Add pagination parameter. Convert page string to integer. diff --git a/src/main/resources/static/cherrypick-status-viewer.js b/src/main/resources/static/cherrypick-status-viewer.js index d6c0ca6..db04284 100644 --- a/src/main/resources/static/cherrypick-status-viewer.js +++ b/src/main/resources/static/cherrypick-status-viewer.js @@ -49,6 +49,7 @@ Gerrit.install(plugin => { this.errorMessage = null; this.changes = []; this.branchColumns = new Set(); + this.selectedBranches = new Set(); this.loading = false; // Read initial state from URL parameters const urlParams = new URLSearchParams(window.location.search); @@ -57,19 +58,124 @@ Gerrit.install(plugin => { repo: urlParams.get('repo') || '', onlyStuck: urlParams.get('onlyStuck') === 'true', exclAbandon: urlParams.get('exclAbandon') === 'true', - onlyExternalTqtc: urlParams.get('onlyExternalTqtc') === 'true' + onlyExternalTqtc: urlParams.get('onlyExternalTqtc') === 'true', + branches: urlParams.get('branches') ? urlParams.get('branches').split(',') : [] // Read branches from URL }; this.currentPage = parseInt(urlParams.get('page') || '1', 10); this.totalPages = 1; + // Initialize selectedBranches from currentFilters + this.selectedBranches = new Set(this.currentFilters.branches); this.shadowRoot.innerHTML = ` <style> - .filter-container { margin-bottom: 1rem; } - .filter-input { margin-right: 1rem; padding: 0.5rem; } - table { width: 100%; border-collapse: collapse; } + /* Main container for all filter groups */ + .filter-container { margin-bottom: 1rem; display: flex; flex-wrap: wrap; align-items: flex-start; gap: 1rem; } + + /* Style for visual grouping of filters */ + .filter-group { + display: flex; + flex-wrap: wrap; /* Allow items to wrap within the group */ + align-items: flex-start; /* Align items to the top */ + gap: 0.5rem; /* Spacing between items in the group */ + padding: 0.5rem; + border: 1px solid #ccc; + border-radius: 4px; + } + + /* Individual filter item (label + input/checkbox) */ + .filter-item { display: flex; align-items: center; gap: 0.25rem; } + + /* Input field styling */ + .filter-input { padding: 0.5rem; } + + /* Label for checkboxes */ + .filter-checkbox-label { margin-left: 0.1rem; margin-right: 0.5rem; } + + /* Wrapper for branch input and tags */ + .branch-filter-wrapper { + display: flex; + flex-direction: column; /* Stack input and tags vertically */ + gap: 0.5rem; /* Space between input row and tags */ + align-items: flex-start; /* Align items to the start */ + } + + /* Container for branch tags */ + .branch-tags-container { + display: flex; /* Keep using flex for tags themselves */ + flex-wrap: wrap; + gap: 0.5rem; + } + .branch-tag { background-color: #e0e0e0; color: black; padding: 0.25rem 0.5rem; border-radius: 4px; display: inline-flex; align-items: center; gap: 0.25rem; } + .remove-branch-btn { cursor: pointer; background: none; border: none; font-weight: bold; color: #555; padding: 0 0.2rem; } + .remove-branch-btn:hover { color: black; } + + /* Tooltip Styles */ + .tooltip-container { + position: relative; + display: inline-block; + } + .filter-item.tooltip-container { + display: flex; + } + .branch-filter-wrapper.tooltip-container { + display: flex; + flex-direction: column; + gap: 0.5rem; + align-items: flex-start; + } + button.tooltip-container { + /* Standard button styles apply */ + } + + + .tooltip-text { + visibility: hidden; + opacity: 0; + width: max-content; + max-width: 250px; + background-color: #555; + color: #fff; + text-align: center; + border-radius: 6px; + padding: 5px 8px; + position: absolute; + z-index: 10; /* Ensure tooltip is on top */ + top: 115%; /* Position below the element */ + left: 50%; + transform: translateX(-50%); + transition: opacity 0.2s ease-in-out; + /* Delay is handled by the hover rule */ + white-space: normal; + font-size: 0.85em; + pointer-events: none; + box-shadow: 0 2px 4px rgba(0,0,0,0.2); + } + + /* Tooltip Arrow */ + .tooltip-text::after { + content: ""; + position: absolute; + bottom: 100%; /* Position arrow at the top of the tooltip */ + left: 50%; + margin-left: -5px; + border-width: 5px; + border-style: solid; + border-color: transparent transparent #555 transparent; /* Arrow pointing up */ + } + + /* Show tooltip on hover with delay */ + .tooltip-container:hover .tooltip-text { + visibility: visible; + opacity: 1; + transition-delay: 0.2s; /* 200ms delay */ + } + /* End Tooltip Styles */ + + + table { width: 100%; border-collapse: collapse; margin-top: 1rem; } th, td { padding: 0.5rem; border: 1px solid #ddd; text-align: left; } - a { color: currentColor;} + a { color: currentColor; } .loading { padding: 1rem; } .error { color: red; } .pagination { margin-top: 1rem; } @@ -86,37 +192,105 @@ Gerrit.install(plugin => { .repo-filter-icon, .owner-filter-icon { position: absolute; - right: 0.5rem; /* Adjust as needed */ + right: 0.5rem; top: 50%; - transform: translateY(-50%); /* Center vertically */ - display: none; /* Initially hidden */ + transform: translateY(-50%); + display: none; + cursor: pointer; + } + + th { /* Make header cells relative for icon positioning */ + position: relative; + padding-right: 2rem; /* Add padding to prevent text overlap */ + } + + .branch-filter-icon { + position: absolute; + right: 0.5rem; + top: 50%; + transform: translateY(-50%); + display: none; cursor: pointer; } td:nth-child(2):hover .repo-filter-icon, /* Repo column */ - td:nth-child(3):hover .owner-filter-icon { /* Owner column */ - display: inline-block; + td:nth-child(3):hover .owner-filter-icon, /* Owner column */ + th:hover .branch-filter-icon { /* Branch header */ + display: inline-block; } </style> <div class="filter-container"> - <label for="ownerFilter">Owner Email:</label> - <input class="filter-input" type="text" id="ownerFilter" list="ownerSuggestions" placeholder="Filter by owner email"> - <datalist id="ownerSuggestions"></datalist> - ${isLoggedIn ? ` - <input type="checkbox" id="onlyMineFilter" name="onlyMineFilter"> - <label for="onlyMineFilter">Only Mine </label> - ` : ''} - <input class="filter-input" type="text" id="repoFilter" list="repoSuggestions" placeholder="Filter by repository"> - <datalist id="repoSuggestions"></datalist> - <input type="checkbox" id="onlyStuckFilter" name="onlyStuckFilter"> - <label for="onlyStuckFilter">Only Stuck Changes </label> - <input type="checkbox" id="exclAbandonFilter" name="exclAbandonFilter"> - <label for="exclAbandonFilter">Exclude Abandon </label> - <input type="checkbox" id="onlyExternalTqtcFilter" name="onlyExternalTqtcFilter" disabled> <!-- Initially disabled --> - <label for="onlyExternalTqtcFilter">Only Externals </label> - <button id="applyFilters">Apply Filters</button> - <button id="clearFilters">Clear Filters</button> + + <!-- Group 1: Text Filters (Owner, Repo, Branch) & Branch Tags --> + <div class="filter-group"> + <!-- Owner Filter --> + <div class="filter-item tooltip-container"> + <label for="ownerFilter">Owner:</label> + <input class="filter-input" type="text" id="ownerFilter" list="ownerSuggestions" placeholder="Filter by owner email"> + <datalist id="ownerSuggestions"></datalist> + <span class="tooltip-text">Filter changes by owner's email address (full match, select from suggestions).</span> + </div> + <!-- Only Mine Filter --> + ${isLoggedIn ? ` + <div class="filter-item tooltip-container" style="padding-top:7px"> + <input type="checkbox" id="onlyMineFilter" name="onlyMineFilter"> + <label for="onlyMineFilter" class="filter-checkbox-label">Only Mine</label> + <span class="tooltip-text">Show only changes owned by you.</span> + </div> + ` : ''} + <!-- Repo Filter --> + <div class="filter-item tooltip-container"> + <label for="repoFilter">Repo:</label> + <input class="filter-input" type="text" id="repoFilter" list="repoSuggestions" placeholder="Filter by repository"> + <datalist id="repoSuggestions"></datalist> + <span class="tooltip-text">Filter changes by repository name (prefix or full match).</span> + </div> + <!-- Branch Filter Wrapper --> + <div class="branch-filter-wrapper"> + <!-- Branch Input/Button --> + <div class="filter-item tooltip-container"> + <label for="branchFilterInput">Branch:</label> + <input class="filter-input" type="text" id="branchFilterInput" placeholder="Add branch filter"> + <button id="addBranchFilterButton">Add</button> + <span class="tooltip-text">Enter a branch name (e.g., dev, 6.5) and click 'Add' to filter by specific branches.</span> + </div> + <div class="branch-tags-container" id="branchFilterTagsContainer"> + <!-- Selected branch tags will be rendered here --> + </div> + </div> + </div> + + <!-- Group 2: Status Checkboxes --> + <div class="filter-group"> + <!-- Only Stuck Filter --> + <div class="filter-item tooltip-container"> + <input type="checkbox" id="onlyStuckFilter" name="onlyStuckFilter"> + <label for="onlyStuckFilter" class="filter-checkbox-label">Only Stuck</label> + <span class="tooltip-text">Show only changes that are not yet merged or actively integrating. (Automatically enabled when filtering by branch).</span> + </div> + <!-- Exclude Abandon Filter --> + <div class="filter-item tooltip-container"> + <input type="checkbox" id="exclAbandonFilter" name="exclAbandonFilter"> + <label for="exclAbandonFilter" class="filter-checkbox-label">Exclude Abandon</label> + <span class="tooltip-text">Hide changes where the only unmerged branch targets are abandoned, or depend directly on an abandonded change. Requires 'Only Stuck' to be enabled.</span> + </div> + <!-- Only Externals Filter --> + <div class="filter-item tooltip-container"> + <input type="checkbox" id="onlyExternalTqtcFilter" name="onlyExternalTqtcFilter" disabled> <!-- Initially disabled --> + <label for="onlyExternalTqtcFilter" class="filter-checkbox-label">Only Externals</label> + <span class="tooltip-text">Show only changes originating from external contributors (non-TQTC). Requires 'Only Stuck' to be enabled.</span> + </div> + </div> + + <!-- Group 3: Actions --> + <div class="filter-group"> + <button id="clearFilters" class="tooltip-container"> + Clear All Filters + <span class="tooltip-text">Remove all active filters and reset the view.</span> + </button> + </div> + </div> <div id="loginMessage" class="error" hidden>This page requires login.</div> @@ -156,8 +330,12 @@ Gerrit.install(plugin => { this.onlyStuckFilterCheckbox = this.shadowRoot.getElementById('onlyStuckFilter'); this.exclAbandonFilterCheckbox = this.shadowRoot.getElementById('exclAbandonFilter'); this.onlyExternalTqtcFilterCheckbox = this.shadowRoot.getElementById('onlyExternalTqtcFilter'); + this.branchFilterInput = this.shadowRoot.getElementById('branchFilterInput'); + this.addBranchFilterButton = this.shadowRoot.getElementById('addBranchFilterButton'); + this.branchFilterTagsContainer = this.shadowRoot.getElementById('branchFilterTagsContainer'); - this.applyFiltersButton?.addEventListener('click', () => this.applyFilters()); + // Remove explicit apply button listener, apply on change/add/remove + // this.applyFiltersButton?.addEventListener('click', () => this.applyFilters()); this.clearFiltersButton?.addEventListener('click', () => this.clearFilters()); this.onlyStuckFilterCheckbox?.addEventListener('change', () => this.handleOnlyStuckFilterChange()); this.exclAbandonFilterCheckbox?.addEventListener('change', () => this.handleExclAbandonFilterChange()); @@ -186,12 +364,85 @@ Gerrit.install(plugin => { this.debouncedFetchRepos.cancel(); this.applyFilters(); }); + + // Branch filter listeners + this.addBranchFilterButton?.addEventListener('click', () => this.addBranchFilter()); + this.branchFilterInput?.addEventListener('keypress', (e) => { + if (e.key === 'Enter') { + this.addBranchFilter(); + } + }); + this.branchFilterTagsContainer?.addEventListener('click', (event) => { + if (event.target.classList.contains('remove-branch-btn')) { + const branchToRemove = event.target.dataset.branch; + this.removeBranchFilter(branchToRemove); + } + }); + this.shadowRoot.getElementById('prevPage')?.addEventListener('click', () => this.handlePageChange(-1)); this.shadowRoot.getElementById('nextPage')?.addEventListener('click', () => this.handlePageChange(1)); } - // Updates the enabled/checked state of dependent checkboxes based on the "Only Stuck" state + addBranchFilter() { + const branchName = this.branchFilterInput.value.trim(); + if (branchName && !this.selectedBranches.has(branchName)) { + this.selectedBranches.add(branchName); + this.branchFilterInput.value = ''; + this._renderBranchTags(); // Update tags UI + this.updateDependentCheckboxStates(); // Update "Only Stuck" state *before* applying filters + this.applyFilters(); // Re-fetch data + } else if (!branchName) { + console.log("Branch name cannot be empty."); + } else { + console.log(`Branch "${branchName}" is already added.`); + } + } + + removeBranchFilter(branchName) { + if (this.selectedBranches.has(branchName)) { + this.selectedBranches.delete(branchName); + this._renderBranchTags(); // Update tags UI + this.updateDependentCheckboxStates(); // Update "Only Stuck" state *before* applying filters + this.applyFilters(); // Re-fetch data + } + } + + _renderBranchTags() { + this.branchFilterTagsContainer.innerHTML = ''; + this.selectedBranches.forEach(branch => { + const tag = document.createElement('span'); + tag.classList.add('branch-tag'); + tag.textContent = branch; + + const removeBtn = document.createElement('button'); + removeBtn.classList.add('remove-branch-btn'); + removeBtn.textContent = '×'; // Multiplication sign as 'x' + removeBtn.dataset.branch = branch; + removeBtn.title = `Remove filter for branch: ${branch}`; + + tag.appendChild(removeBtn); + this.branchFilterTagsContainer.appendChild(tag); + }); + } + + // Updates the enabled/checked state of "Only Stuck" based on branch filters, + // and the state of dependent checkboxes based on the "Only Stuck" state. updateDependentCheckboxStates() { + const hasBranchFilters = this.selectedBranches.size > 0; + + if (hasBranchFilters) { + // Force "Only Stuck" on and disable it when branch filters are active + this.onlyStuckFilterCheckbox.checked = true; + this.onlyStuckFilterCheckbox.disabled = true; + this.currentFilters.onlyStuck = true; + } else { + // Enable "Only Stuck" if no branch filters are active + this.onlyStuckFilterCheckbox.disabled = false; + // Restore checked state based on currentFilters (which might be from URL or prior user action) + this.onlyStuckFilterCheckbox.checked = this.currentFilters.onlyStuck; + } + + // Use the potentially forced state of isStuckChecked const isStuckChecked = this.onlyStuckFilterCheckbox.checked; // Handle "Exclude Abandon" checkbox @@ -199,18 +450,30 @@ Gerrit.install(plugin => { if (!isStuckChecked) { this.exclAbandonFilterCheckbox.checked = false; this.currentFilters.exclAbandon = false; + } else { + // If stuck is checked, ensure the checkbox reflects the filter state + // (it might have been loaded from URL or set by user before branches were added) + this.exclAbandonFilterCheckbox.checked = this.currentFilters.exclAbandon; } + // Handle "Only Externals" checkbox const shouldTqtcBeEnabled = isStuckChecked; this.onlyExternalTqtcFilterCheckbox.disabled = !shouldTqtcBeEnabled; if (!shouldTqtcBeEnabled) { this.onlyExternalTqtcFilterCheckbox.checked = false; this.currentFilters.onlyExternalTqtc = false; + } else { + // If stuck is checked, ensure the checkbox reflects the filter state + this.onlyExternalTqtcFilterCheckbox.checked = this.currentFilters.onlyExternalTqtc; } } handleOnlyStuckFilterChange() { + // Prevent manual changes if the checkbox is disabled (e.g., by branch filter) + if (this.onlyStuckFilterCheckbox.disabled) { + return; + } this.currentFilters.onlyStuck = this.onlyStuckFilterCheckbox.checked; this.updateDependentCheckboxStates(); // Update dependent checkboxes this.applyFilters(); @@ -239,16 +502,22 @@ Gerrit.install(plugin => { this.ownerFilterInput.value = this.currentFilters.owner; this.repoFilterInput.value = this.currentFilters.repo; this.onlyStuckFilterCheckbox.checked = this.currentFilters.onlyStuck; - this.exclAbandonFilterCheckbox.checked = this.currentFilters.exclAbandon; - this.onlyExternalTqtcFilterCheckbox.checked = this.currentFilters.onlyExternalTqtc; + // Note: exclAbandon and onlyExternalTqtc are handled by updateDependentCheckboxStates + // Set initial enabled/disabled states and potentially correct checked states this.updateDependentCheckboxStates(); + // Ensure the actual checked state matches the filter state after dependency logic + this.exclAbandonFilterCheckbox.checked = this.currentFilters.exclAbandon && !this.exclAbandonFilterCheckbox.disabled; + this.onlyExternalTqtcFilterCheckbox.checked = this.currentFilters.onlyExternalTqtc && !this.onlyExternalTqtcFilterCheckbox.disabled; + // Handle 'Only Mine' checkbox state based on loaded owner filter if (this.onlyMineFilterCheckbox) { this.onlyMineFilterCheckbox.checked = !!this.currentFilters.owner; } + // Render initial branch tags + this._renderBranchTags(); // Fetch data using initial filters (potentially from URL) this.fetchData(); // Add listener for browser back/forward navigation @@ -268,18 +537,22 @@ Gerrit.install(plugin => { repo: urlParams.get('repo') || '', onlyStuck: urlParams.get('onlyStuck') === 'true', exclAbandon: urlParams.get('exclAbandon') === 'true', - onlyExternalTqtc: urlParams.get('onlyExternalTqtc') === 'true' + onlyExternalTqtc: urlParams.get('onlyExternalTqtc') === 'true', + branches: urlParams.get('branches') ? urlParams.get('branches').split(',') : [] }; this.currentPage = parseInt(urlParams.get('page') || '1', 10); + this.selectedBranches = new Set(this.currentFilters.branches); // Update selected branches set // Update UI elements to reflect the state from the URL this.ownerFilterInput.value = this.currentFilters.owner; this.repoFilterInput.value = this.currentFilters.repo; this.onlyStuckFilterCheckbox.checked = this.currentFilters.onlyStuck; - this.exclAbandonFilterCheckbox.checked = this.currentFilters.exclAbandon; - this.onlyExternalTqtcFilterCheckbox.checked = this.currentFilters.onlyExternalTqtc; // Restore enabled/disabled states and potentially correct checked states this.updateDependentCheckboxStates(); + // Ensure the actual checked state matches the filter state after dependency logic + this.exclAbandonFilterCheckbox.checked = this.currentFilters.exclAbandon && !this.exclAbandonFilterCheckbox.disabled; + this.onlyExternalTqtcFilterCheckbox.checked = this.currentFilters.onlyExternalTqtc && !this.onlyExternalTqtcFilterCheckbox.disabled; + if (this.onlyMineFilterCheckbox) { // If owner matches logged-in user's email (fetched elsewhere), check 'Only Mine' @@ -288,6 +561,8 @@ Gerrit.install(plugin => { } } + // Re-render branch tags + this._renderBranchTags(); // Re-fetch data based on the restored state this.fetchData(false); // Pass false to prevent pushing state again } @@ -435,6 +710,9 @@ Gerrit.install(plugin => { if (this.currentFilters.onlyStuck) params.set('onlyStuck', 'true'); if (this.currentFilters.exclAbandon) params.set('exclAbandon', 'true'); if (this.currentFilters.onlyExternalTqtc) params.set('onlyExternalTqtc', 'true'); + if (this.selectedBranches.size > 0) { + params.set('branches', Array.from(this.selectedBranches).join(',')); + } params.set('page', this.currentPage); const response = await fetch(`/config/server/getdashboard?${params.toString()}`) @@ -469,6 +747,9 @@ Gerrit.install(plugin => { if (this.currentFilters.onlyStuck) params.set('onlyStuck', 'true'); if (this.currentFilters.exclAbandon) params.set('exclAbandon', 'true'); if (this.currentFilters.onlyExternalTqtc) params.set('onlyExternalTqtc', 'true'); + if (this.selectedBranches.size > 0) { + params.set('branches', Array.from(this.selectedBranches).join(',')); + } if (this.currentPage > 1) params.set('page', this.currentPage); // Only add page if > 1 const newUrl = `${window.location.pathname}?${params.toString()}`; @@ -500,7 +781,10 @@ Gerrit.install(plugin => { } this.currentFilters.onlyStuck = false; this.currentFilters.exclAbandon = false; - this.currentFilters.onlyExternalTqtc = false; // Reset filter states + this.currentFilters.onlyExternalTqtc = false; + this.selectedBranches.clear(); + this.branchFilterInput.value = ''; + this._renderBranchTags(); // Clear branch tags UI this.updateDependentCheckboxStates(); // Ensure dependent checkboxes are disabled and unchecked this.repoSuggestionsDatalist.innerHTML = ''; this.ownerSuggestionsDatalist.innerHTML = ''; @@ -523,6 +807,7 @@ Gerrit.install(plugin => { const errorEl = this.shadowRoot.querySelector('.error'); // General error display const tableEl = this.shadowRoot.querySelector('table'); const paginationEl = this.shadowRoot.querySelector('.pagination'); + const branchTagsContainerEl = this.shadowRoot.getElementById('branchFilterTagsContainer'); const currentPageEl = this.shadowRoot.getElementById('currentPage'); const totalPagesEl = this.shadowRoot.getElementById('totalPages'); const prevPageButton = this.shadowRoot.getElementById('prevPage'); @@ -543,6 +828,10 @@ Gerrit.install(plugin => { // Proceed with normal rendering if logged in loginMessageEl.hidden = true; // Ensure login message is hidden if (filterContainerEl) filterContainerEl.hidden = false; // Show filters + // Hide branch tags container if it's empty using display style + if (branchTagsContainerEl) { + branchTagsContainerEl.style.display = this.selectedBranches.size === 0 ? 'none' : 'flex'; + } loadingEl.hidden = !this.loading; errorEl.hidden = !this.errorMessage; // Show/hide general error based on state @@ -555,6 +844,9 @@ Gerrit.install(plugin => { return; // Stop rendering if there's an error } + // Render branch tags (might be redundant if called elsewhere, but ensures consistency) + this._renderBranchTags(); + currentPageEl.textContent = this.currentPage; totalPagesEl.textContent = this.totalPages; prevPageButton.disabled = this.currentPage === 1; @@ -567,7 +859,13 @@ Gerrit.install(plugin => { <th>Repo</th> <th>Owner Email</th> <th>Merged Date</th> - ${Array.from(this.branchColumns).map(b => `<th>${b}</th>`).join('')} + ${Array.from(this.branchColumns).map(b => ` + <th> + ${b} + <span class="branch-filter-icon" data-branch="${b}" title="Filter by this branch"> + 🔍 + </span> + </th>`).join('')} `; const tbody = this.shadowRoot.querySelector('tbody'); @@ -628,6 +926,19 @@ Gerrit.install(plugin => { this.applyFilters(); }); }); + + // Add listeners for branch header filter icons + thead.querySelectorAll('.branch-filter-icon').forEach(icon => { + icon.addEventListener('click', (event) => { + const branchName = event.target.dataset.branch; + if (branchName && !this.selectedBranches.has(branchName)) { + this.selectedBranches.add(branchName); + this._renderBranchTags(); // Update tags UI immediately + this.updateDependentCheckboxStates(); // Update "Only Stuck" state + this.applyFilters(); // Re-fetch data + } + }); + }); } } |