aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorDaniel Smith <[email protected]>2025-06-19 11:12:47 +0200
committerDaniel Smith <[email protected]>2025-06-30 09:54:48 +0000
commit42aeaf259e2281683299794c508e1d8c7812806a (patch)
treee7953bcbf16c6efa5cd08b836703d05737d7c150
parentc39a21a4154e7f89bb261e6b94de47c8ba352963 (diff)
Add Change ID/SHA and Free Text search to Cherry Pick Status ViewerHEADdev
-rw-r--r--src/main/java/com/google/gerrit/plugins/cherrypickstatus/CherryPickRestApi.java93
-rw-r--r--src/main/resources/static/cherrypick-status-viewer.js608
2 files changed, 512 insertions, 189 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 7983f32..d7c14e4 100644
--- a/src/main/java/com/google/gerrit/plugins/cherrypickstatus/CherryPickRestApi.java
+++ b/src/main/java/com/google/gerrit/plugins/cherrypickstatus/CherryPickRestApi.java
@@ -121,6 +121,22 @@ public class CherryPickRestApi {
this.hashtags = hashtags;
}
+ // Option for filtering by changeId or sha1. Populated from query parameters.
+ @Option(name = "--changeid", usage = "Gerrit Change-Id or commit sha1")
+ private String changeId;
+
+ public void setChangeId(String changeId) {
+ this.changeId = changeId;
+ }
+
+ // Option for free-text search on subject. Populated from query parameters.
+ @Option(name = "--subject", usage = "Free-text search for subject line")
+ private String subject;
+
+ public void setSubject(String subject) {
+ this.subject = subject;
+ }
+
// Constructor for dependency injection. Initializes fields.
@Inject
public GetDashboard(
@@ -141,15 +157,15 @@ public class CherryPickRestApi {
// Determines the set of projects visible to the current user based on READ permissions.
// Filters by repoName if provided, otherwise checks all projects.
- public Set<Project.NameKey> getVisibleProjects(CurrentUser user)
+ public Set<Project.NameKey> getVisibleProjects(CurrentUser user, String repoFilter)
throws PermissionBackendException {
Set<Project.NameKey> visibleProjects = new TreeSet<>(); // Use TreeSet for sorted order.
try {
PermissionBackend.WithUser withUser = permissionBackend.user(user);
// If a specific repository is requested, check permission for that one.
- if (repoName != null && !repoName.isEmpty()) {
- Project.NameKey repoKey = Project.NameKey.parse(repoName);
+ if (repoFilter != null && !repoFilter.isEmpty()) {
+ Project.NameKey repoKey = Project.NameKey.parse(repoFilter);
// Filter the single repo based on user's READ permission.
visibleProjects.addAll(withUser.filter(ProjectPermission.READ, Set.of(repoKey)));
} else {
@@ -203,7 +219,10 @@ public class CherryPickRestApi {
// Determine visible projects and add them to the request body.
try {
- Set<Project.NameKey> visibleProjects = getVisibleProjects(user);
+ // If searching by changeId, ignore the repoName filter from the query params.
+ String effectiveRepoFilter =
+ (changeId != null && !changeId.isEmpty()) ? null : repoName;
+ Set<Project.NameKey> visibleProjects = getVisibleProjects(user, effectiveRepoFilter);
List<String> repos =
visibleProjects.stream().map(Project.NameKey::get).collect(Collectors.toList());
requestBody.put("repos", repos); // Add list of visible repo names.
@@ -213,35 +232,43 @@ public class CherryPickRestApi {
}
// Add optional filters to the request body if they are set.
- if (owner != null && !owner.isEmpty()) {
- requestBody.put("owner", owner);
- }
- if (onlyStuck) {
- requestBody.put("onlyStuck", true);
- }
- if (exclAbandon) {
- requestBody.put("exclAbandon", true);
- }
- if (onlyExternalTqtc) {
- 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
- }
- if (hashtags != null && !hashtags.isEmpty()) {
- // Split the comma-separated string into a list of hashtag names
- List<String> hashtagList = List.of(hashtags.split(","));
- requestBody.put("hashtags", hashtagList); // Pass hashtags as a list of strings
- }
-
- // Add pagination parameter. Convert page string to integer.
- try {
- requestBody.put("page", Integer.parseInt(page));
- } catch (NumberFormatException e) {
- log.warn("Invalid page parameter received: {}", page);
- throw RestApiException.wrap("Invalid page parameter: " + page, e);
+ if (changeId != null && !changeId.isEmpty()) {
+ requestBody.put("changeId", changeId);
+ // When searching by changeId, other filters and pagination are ignored by the backend.
+ } else {
+ if (subject != null && !subject.isEmpty()) {
+ requestBody.put("subject", subject);
+ }
+ if (owner != null && !owner.isEmpty()) {
+ requestBody.put("owner", owner);
+ }
+ if (onlyStuck) {
+ requestBody.put("onlyStuck", true);
+ }
+ if (exclAbandon) {
+ requestBody.put("exclAbandon", true);
+ }
+ if (onlyExternalTqtc) {
+ 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
+ }
+ if (hashtags != null && !hashtags.isEmpty()) {
+ // Split the comma-separated string into a list of hashtag names
+ List<String> hashtagList = List.of(hashtags.split(","));
+ requestBody.put("hashtags", hashtagList); // Pass hashtags as a list of strings
+ }
+
+ // Add pagination parameter. Convert page string to integer.
+ try {
+ requestBody.put("page", Integer.parseInt(page));
+ } catch (NumberFormatException e) {
+ log.warn("Invalid page parameter received: {}", page);
+ throw RestApiException.wrap("Invalid page parameter: " + page, e);
+ }
}
// Serialize the request body to JSON.
diff --git a/src/main/resources/static/cherrypick-status-viewer.js b/src/main/resources/static/cherrypick-status-viewer.js
index 2747d60..78049e8 100644
--- a/src/main/resources/static/cherrypick-status-viewer.js
+++ b/src/main/resources/static/cherrypick-status-viewer.js
@@ -40,6 +40,51 @@ Gerrit.install(plugin => {
return debounced;
}
+ function getStatusByEnum(status, dependsOn) {
+ const statusToColor = {
+ NOT_CREATED: {
+ textOverride: dependsOn
+ ? `Depends on ${dependsOn}` : 'Not yet picked', bg: 'lightgrey', text: 'black'
+ },
+ NEW: { textOverride: "New", bg: 'hsl(48, 100%, 50%)', text: 'black' },
+ MERGE_CONFLICT: { textOverride: "Merge conflict", bg: 'orange', text: 'black' },
+ MERGED: { textOverride: "Merged", bg: 'hsl(120, 100%, 30%)', text: 'white' },
+ ABANDONED: { textOverride: "Abandoned", bg: '#ccccff', text: 'black' },
+ DEFERRED: { textOverride: "Deferred", bg: '#ffccff', text: 'black' },
+ STAGED: { textOverride: "Integrating", bg: 'rgb(204, 255, 153)', text: 'black' },
+ INTEGRATING: { textOverride: "Integrating", bg: 'rgb(204, 255, 153)', text: 'black' },
+ FAILED_CI: { textOverride: "CI Failed", bg: 'darkred', text: 'white' }
+ };
+ return statusToColor[status] || { bg: '', text: '' };
+ }
+
+ function sortBranchNames(branches) {
+ return branches.sort((a, b) => {
+ const [aMajor, aMinor, aPatch] = a.split('.').map(Number);
+ const [bMajor, bMinor, bPatch] = b.split('.').map(Number);
+
+ if (!(a.match(/\d/))) {
+ if (a === "master" || a === "dev")
+ return -1;
+ else if (a.match(/\w/))
+ return 1;
+ }
+ if (aMajor !== bMajor) {
+ return bMajor - aMajor;
+ }
+ if (aMinor !== bMinor) {
+ return bMinor - aMinor;
+ }
+ if (aPatch === undefined) {
+ return -1;
+ }
+ if (bPatch === undefined) {
+ return 1;
+ }
+ return (bPatch || 0) - (aPatch || 0);
+ });
+ }
+
class CherryPickMain extends HTMLElement {
constructor() {
super();
@@ -55,6 +100,8 @@ Gerrit.install(plugin => {
// Read initial state from URL parameters
const urlParams = new URLSearchParams(window.location.search);
this.currentFilters = {
+ changeid: urlParams.get('changeid') || '',
+ subject: urlParams.get('subject') || '',
owner: urlParams.get('owner') || '',
repo: urlParams.get('repo') || '',
onlyStuck: urlParams.get('onlyStuck') === 'true',
@@ -239,33 +286,33 @@ Gerrit.install(plugin => {
th:hover .branch-filter-icon { /* Branch header */
display: inline-block;
}
+
+ .header-wrapper {
+ display: flex;
+ align-items: center;
+ gap: 0.5rem;
+ }
+ .header-input {
+ width: 130px; /* Enough for placeholder */
+ transition: width 0.25s ease-in-out;
+ padding: 0.25rem;
+ border: 1px solid #ccc;
+ border-radius: 3px;
+ }
+ .header-input:focus {
+ width: 100%; /* Expand to fill flex container */
+ }
</style>
<div class="filter-container">
<!-- Group 1: Text Filters (Owner, Repo, Branch) & Branch Tags -->
<div class="filter-group">
- <!-- Owner Filter -->
+ <!-- Change ID 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>
+ <label for="changeIdFilter">Change ID/SHA1:</label>
+ <input class="filter-input" type="text" id="changeIdFilter" placeholder="Gerrit Change-Id or SHA1">
+ <span class="tooltip-text">Search by Gerrit Change-Id (I-prefixed) or commit SHA1 (7-40 hex chars). This ignores other filters.</span>
</div>
<!-- Branch Filter Wrapper -->
<div class="branch-filter-wrapper">
@@ -298,18 +345,26 @@ Gerrit.install(plugin => {
<!-- 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 -->
+ <!-- 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 Mine Filter -->
+ ${isLoggedIn ? `
<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>
+ <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>
+ ` : ''}
<!-- Only Externals Filter -->
<div class="filter-item tooltip-container">
<input type="checkbox" id="onlyExternalTqtcFilter" name="onlyExternalTqtcFilter" disabled> <!-- Initially disabled -->
@@ -350,18 +405,17 @@ Gerrit.install(plugin => {
<button id="nextPage">&gt;</button>
</div>
`;
- if (isLoggedIn) {
- this.onlyMineFilterCheckbox = this.shadowRoot.getElementById('onlyMineFilter');
- this.onlyMineFilterCheckbox?.addEventListener('change', () =>
- this.handleOnlyMineFilterChange()); // Use optional chaining
- }
-
- this.ownerFilterInput = this.shadowRoot.getElementById('ownerFilter');
- this.repoFilterInput = this.shadowRoot.getElementById('repoFilter');
+ this.onlyMineFilterCheckbox = null;
+ this.ownerFilterInput = null;
+ this.repoFilterInput = null;
+ this.repoSuggestionsDatalist = null;
+ this.ownerSuggestionsDatalist = null;
+ this.subjectSearchInput = null; // Will be assigned in _render
+ this.userEmail = null;
+
+ this.changeIdFilterInput = this.shadowRoot.getElementById('changeIdFilter');
this.applyFiltersButton = this.shadowRoot.getElementById('applyFilters');
this.clearFiltersButton = this.shadowRoot.getElementById('clearFilters');
- this.repoSuggestionsDatalist = this.shadowRoot.getElementById('repoSuggestions');
- this.ownerSuggestionsDatalist = this.shadowRoot.getElementById('ownerSuggestions');
this.onlyStuckFilterCheckbox = this.shadowRoot.getElementById('onlyStuckFilter');
this.exclAbandonFilterCheckbox = this.shadowRoot.getElementById('exclAbandonFilter');
this.onlyExternalTqtcFilterCheckbox = this.shadowRoot.getElementById('onlyExternalTqtcFilter');
@@ -373,6 +427,15 @@ Gerrit.install(plugin => {
this.hashtagFilterTagsContainer = this.shadowRoot.getElementById('hashtagFilterTagsContainer');
this.hashtagSuggestionsDatalist = this.shadowRoot.getElementById('hashtagSuggestions');
+ // Change ID filter listeners
+ this.changeIdFilterInput?.addEventListener('input', () => this.updateFilterStates());
+ this.changeIdFilterInput?.addEventListener('change', () => this.applyFilters());
+ this.changeIdFilterInput?.addEventListener('keypress', (e) => {
+ if (e.key === 'Enter') {
+ this.applyFilters();
+ }
+ });
+
// Remove explicit apply button listener, apply on change/add/remove
// this.applyFiltersButton?.addEventListener('click', () => this.applyFilters());
this.clearFiltersButton?.addEventListener('click', () => this.clearFilters());
@@ -382,27 +445,8 @@ Gerrit.install(plugin => {
// Debounce owner input for API calls
this.debouncedFetchOwners = debounce(this.fetchOwnerSuggestions.bind(this), 300);
- this.ownerFilterInput?.addEventListener('input', () => {
- this.currentFilters.owner = this.ownerFilterInput.value;
- this.debouncedFetchOwners(this.ownerFilterInput.value); // Fetch suggestions on input
- });
- this.ownerFilterInput?.addEventListener('change', () => {
- // On change (selection, enter, blur), cancel pending fetches and apply
- this.debouncedFetchOwners.cancel();
- this.applyFilters();
- });
-
// Debounce repo input for API calls
this.debouncedFetchRepos = debounce(this.fetchRepoSuggestions.bind(this), 300);
- this.repoFilterInput?.addEventListener('input', () => {
- this.currentFilters.repo = this.repoFilterInput.value;
- this.debouncedFetchRepos(this.repoFilterInput.value); // Fetch suggestions on input
- });
- this.repoFilterInput?.addEventListener('change', () => {
- // On change (selection, enter, blur), cancel pending fetches and apply
- this.debouncedFetchRepos.cancel();
- this.applyFilters();
- });
// Branch filter listeners
this.addBranchFilterButton?.addEventListener('click', () => this.addBranchFilter());
@@ -641,25 +685,37 @@ Gerrit.install(plugin => {
this.applyFilters();
}
- connectedCallback() {
+ async fetchUserEmail() {
+ if (!isLoggedIn || this.userEmail) return;
+ try {
+ const response = await fetch('http://code.qt.io/a/accounts/self');
+ if (!response.ok) {
+ throw new Error(`HTTP error! status: ${response.status}`);
+ }
+ const text = await response.text();
+ const data = JSON.parse(text.substring(4));
+ this.userEmail = data.email;
+ } catch (error) {
+ console.error('Error fetching user email:', error);
+ }
+ }
+
+ async connectedCallback() {
// Set initial input/checkbox values from currentFilters (read from URL in constructor)
- this.ownerFilterInput.value = this.currentFilters.owner;
- this.repoFilterInput.value = this.currentFilters.repo;
+ this.changeIdFilterInput.value = this.currentFilters.changeid;
this.onlyStuckFilterCheckbox.checked = this.currentFilters.onlyStuck;
// Note: exclAbandon and onlyExternalTqtc are handled by updateDependentCheckboxStates
+ // We need user's email to correctly determine initial state of "Only Mine" checkbox
+ await this.fetchUserEmail();
+
// Set initial enabled/disabled states and potentially correct checked states
+ this.updateFilterStates();
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();
this._renderHashtagTags();
@@ -676,9 +732,11 @@ Gerrit.install(plugin => {
}
// Handle browser back/forward button clicks
- handlePopState(event) {
+ async handlePopState(event) {
const urlParams = new URLSearchParams(window.location.search);
this.currentFilters = {
+ changeid: urlParams.get('changeid') || '',
+ subject: urlParams.get('subject') || '',
owner: urlParams.get('owner') || '',
repo: urlParams.get('repo') || '',
onlyStuck: urlParams.get('onlyStuck') === 'true',
@@ -691,24 +749,19 @@ Gerrit.install(plugin => {
this.selectedBranches = new Set(this.currentFilters.branches);
this.selectedHashtags = new Set(this.currentFilters.hashtags);
+ // We need user's email to correctly determine initial state of "Only Mine" checkbox
+ await this.fetchUserEmail();
+
// Update UI elements to reflect the state from the URL
- this.ownerFilterInput.value = this.currentFilters.owner;
- this.repoFilterInput.value = this.currentFilters.repo;
+ this.changeIdFilterInput.value = this.currentFilters.changeid;
this.onlyStuckFilterCheckbox.checked = this.currentFilters.onlyStuck;
// Restore enabled/disabled states and potentially correct checked states
+ this.updateFilterStates();
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'
- if (!this.currentFilters.owner) {
- this.onlyMineFilterCheckbox.checked = false;
- }
- }
-
// Re-render branch tags
this._renderBranchTags();
this._renderHashtagTags();
@@ -720,33 +773,6 @@ Gerrit.install(plugin => {
this.isLoggedIn = await restApi.getLoggedIn();
}
- sortBranchNames(branches) {
- return branches.sort((a, b) => {
- const [aMajor, aMinor, aPatch] = a.split('.').map(Number);
- const [bMajor, bMinor, bPatch] = b.split('.').map(Number);
-
- if (!(a.match(/\d/))) {
- if (a === "master" || a === "dev")
- return -1;
- else if (a.match(/\w/))
- return 1;
- }
- if (aMajor !== bMajor) {
- return bMajor - aMajor;
- }
- if (aMinor !== bMinor) {
- return bMinor - aMinor;
- }
- if (aPatch === undefined) {
- return -1;
- }
- if (bPatch === undefined) {
- return 1;
- }
- return (bPatch || 0) - (aPatch || 0);
- });
- }
-
getContrastColor(hexColor) {
// Convert hex color to RGB
const r = parseInt(hexColor.substring(1, 3), 16);
@@ -760,44 +786,23 @@ Gerrit.install(plugin => {
return luminance > 0.5 ? '#000000' : '#ffffff';
}
- getStatusByEnum(status, dependsOn) {
- const statusToColor = {
- NOT_CREATED: {
- textOverride: dependsOn
- ? `Depends on ${dependsOn}` : 'Not yet picked', bg: 'lightgrey', text: 'black'
- },
- NEW: { textOverride: "New", bg: 'hsl(48, 100%, 50%)', text: 'black' },
- MERGE_CONFLICT: { textOverride: "Merge conflict", bg: 'orange', text: 'black' },
- MERGED: { textOverride: "Merged", bg: 'hsl(120, 100%, 30%)', text: 'white' },
- ABANDONED: { textOverride: "Abandoned", bg: '#ccccff', text: 'black' },
- DEFERRED: { textOverride: "Deferred", bg: '#ffccff', text: 'black' },
- STAGED: { textOverride: "Integrating", bg: 'rgb(204, 255, 153)', text: 'black' },
- INTEGRATING: { textOverride: "Integrating", bg: 'rgb(204, 255, 153)', text: 'black' },
- FAILED_CI: { textOverride: "CI Failed", bg: 'darkred', text: 'white' }
- };
- return statusToColor[status] || { bg: '', text: '' };
- }
-
async handleOnlyMineFilterChange() {
if (this.onlyMineFilterCheckbox && this.onlyMineFilterCheckbox.checked) {
- try {
- const response = await fetch('http://code.qt.io/a/accounts/self');
- if (!response.ok) {
- throw new Error(`HTTP error! status: ${response.status}`);
- }
- // trim response of leading ")]}'" characters
- const text = await response.text();
- const data = JSON.parse(text.substring(4));
- this.ownerFilterInput.value = data.email;
- this.currentFilters.owner = data.email;
- } catch (error) {
- console.error('Error fetching user email:', error);
- this.errorMessage = 'Error fetching user email. Please try again later.';
- this._render();
- return;
+ await this.fetchUserEmail(); // Ensure we have the user's email
+
+ if (this.userEmail) {
+ if (this.ownerFilterInput) this.ownerFilterInput.value = this.userEmail;
+ this.currentFilters.owner = this.userEmail;
+ } else {
+ console.error("Could not apply 'Only Mine' filter: user email not available.");
+ // Revert the checkbox state as we couldn't fulfill the request
+ this.onlyMineFilterCheckbox.checked = false;
+ this.errorMessage = 'Error fetching your user email. Cannot apply "Only Mine" filter.';
+ this._render();
+ return;
}
} else {
- this.ownerFilterInput.value = '';
+ if (this.ownerFilterInput) this.ownerFilterInput.value = '';
this.currentFilters.owner = '';
}
this.applyFilters();
@@ -805,8 +810,8 @@ Gerrit.install(plugin => {
async fetchRepoSuggestions(prefix) {
// Don't search for empty or very short strings, or if restApi not ready
- if (!prefix || prefix.length < 1 || !restApi) {
- this.repoSuggestionsDatalist.innerHTML = '';
+ if (!prefix || prefix.length < 1 || !restApi || !this.repoSuggestionsDatalist) {
+ if (this.repoSuggestionsDatalist) this.repoSuggestionsDatalist.innerHTML = '';
return;
}
try {
@@ -824,8 +829,8 @@ Gerrit.install(plugin => {
async fetchOwnerSuggestions(prefix) {
// Don't search for empty or very short strings, or if restApi not ready
- if (!prefix || prefix.length < 1 || !restApi) {
- this.ownerSuggestionsDatalist.innerHTML = '';
+ if (!prefix || prefix.length < 1 || !restApi || !this.ownerSuggestionsDatalist) {
+ if (this.ownerSuggestionsDatalist) this.ownerSuggestionsDatalist.innerHTML = '';
return;
}
try {
@@ -883,6 +888,8 @@ Gerrit.install(plugin => {
try {
const params = new URLSearchParams();
+ if (this.currentFilters.changeid) params.set('changeid', this.currentFilters.changeid);
+ if (this.currentFilters.subject) params.set('subject', this.currentFilters.subject);
if (this.currentFilters.owner) params.set('owner', this.currentFilters.owner);
if (this.currentFilters.repo) params.set('repo', this.currentFilters.repo);
if (this.currentFilters.onlyStuck) params.set('onlyStuck', 'true');
@@ -908,7 +915,7 @@ Gerrit.install(plugin => {
}
}
- this.branchColumns = new Set(this.sortBranchNames(Array.from(allBranches)));
+ this.branchColumns = new Set(sortBranchNames(Array.from(allBranches)));
this.totalPages = data.totalPages || 1; // Ensure totalPages is at least 1
this.errorMessage = null;
@@ -923,6 +930,8 @@ Gerrit.install(plugin => {
// Function to update the browser URL
updateURL() {
const params = new URLSearchParams();
+ if (this.currentFilters.changeid) params.set('changeid', this.currentFilters.changeid);
+ if (this.currentFilters.subject) params.set('subject', this.currentFilters.subject);
if (this.currentFilters.owner) params.set('owner', this.currentFilters.owner);
if (this.currentFilters.repo) params.set('repo', this.currentFilters.repo);
if (this.currentFilters.onlyStuck) params.set('onlyStuck', 'true');
@@ -943,15 +952,88 @@ Gerrit.install(plugin => {
history.pushState({ filters: this.currentFilters, page: this.currentPage }, '', newUrl);
}
+ updateFilterStates() {
+ const hasChangeId = this.changeIdFilterInput.value.trim() !== '';
+ // Subject search is no longer an exclusive filter on its own.
+
+ // Only ChangeID is an exclusive filter.
+ const hasExclusiveFilter = hasChangeId;
+
+ // All other filters are disabled if ChangeID is present.
+ const fieldsToDisable = [
+ this.ownerFilterInput, this.repoFilterInput,
+ this.onlyStuckFilterCheckbox, this.exclAbandonFilterCheckbox,
+ this.onlyExternalTqtcFilterCheckbox, this.branchFilterInput,
+ this.addBranchFilterButton, this.hashtagFilterInput,
+ this.addHashtagFilterButton, this.onlyMineFilterCheckbox,
+ this.subjectSearchInput // Subject input is disabled by ChangeID
+ ];
+
+ fieldsToDisable.forEach(field => {
+ if (field) field.disabled = hasExclusiveFilter;
+ });
+
+ // ChangeID input is never disabled by other filters.
+ // Subject input is disabled by ChangeID (handled above).
+
+ this.shadowRoot.querySelectorAll('.remove-branch-btn, .remove-hashtag-btn').forEach(btn => {
+ btn.disabled = hasExclusiveFilter;
+ });
+
+ if (!hasExclusiveFilter) {
+ // If no exclusive filter, manage dependent checkboxes normally.
+ this.updateDependentCheckboxStates();
+ } else {
+ // If we are disabling via changeId, ensure dependent checkboxes are also disabled.
+ this.onlyStuckFilterCheckbox.disabled = true;
+ this.exclAbandonFilterCheckbox.disabled = true;
+ this.onlyExternalTqtcFilterCheckbox.disabled = true;
+ }
+ }
applyFilters() {
+ this.currentFilters.changeid = this.changeIdFilterInput.value.trim();
+ if (this.subjectSearchInput) {
+ this.currentFilters.subject = this.subjectSearchInput.value.trim();
+ }
+ if (this.repoFilterInput) {
+ this.currentFilters.repo = this.repoFilterInput.value.trim();
+ }
+ if (this.ownerFilterInput) {
+ this.currentFilters.owner = this.ownerFilterInput.value.trim();
+ }
+
+ const changeId = this.currentFilters.changeid;
+ if (changeId) {
+ const isGerritId = /^I[0-9a-f]{40}$/i.test(changeId);
+ const isSha1 = /^[0-9a-f]{7,40}$/i.test(changeId);
+ if (!isGerritId && !isSha1) {
+ this.errorMessage = `Invalid Change ID or SHA1. A Gerrit Change-ID must be 'I' followed by 40 hex chars. A SHA1 must be 7-40 hex chars.`;
+ this.loading = false;
+ this._render();
+ return;
+ }
+ }
+ this.errorMessage = null; // Clear previous validation error
+
+ this.updateFilterStates();
this.currentPage = 1; // Reset to first page when filters change
this.fetchData(); // fetchData will update the URL
}
clearFilters() {
- this.ownerFilterInput.value = '';
- this.repoFilterInput.value = '';
+ this.changeIdFilterInput.value = '';
+ if (this.subjectSearchInput) {
+ this.subjectSearchInput.value = '';
+ }
+ if (this.ownerFilterInput) {
+ this.ownerFilterInput.value = '';
+ }
+ if (this.repoFilterInput) {
+ this.repoFilterInput.value = '';
+ }
+ this.currentFilters.changeid = '';
+ this.currentFilters.subject = '';
this.currentFilters.owner = '';
this.currentFilters.repo = '';
if (this.onlyMineFilterCheckbox) {
@@ -972,9 +1054,9 @@ Gerrit.install(plugin => {
this.selectedHashtags.clear();
this.hashtagFilterInput.value = '';
this._renderHashtagTags();
- this.updateDependentCheckboxStates(); // Ensure dependent checkboxes are disabled and unchecked
- this.repoSuggestionsDatalist.innerHTML = '';
- this.ownerSuggestionsDatalist.innerHTML = '';
+ this.updateFilterStates(); // Re-enable all filters
+ if (this.repoSuggestionsDatalist) this.repoSuggestionsDatalist.innerHTML = '';
+ if (this.ownerSuggestionsDatalist) this.ownerSuggestionsDatalist.innerHTML = '';
this.applyFilters(); // Fetch data with cleared filters
}
@@ -1047,9 +1129,26 @@ Gerrit.install(plugin => {
const thead = this.shadowRoot.querySelector('thead tr');
thead.innerHTML = `
- <th>Subject</th>
- <th>Repo</th>
- <th>Owner Email</th>
+ <th>
+ <div class="header-wrapper">
+ <span>Subject</span>
+ <input type="text" id="subjectSearchInput" class="header-input" placeholder="Search subject..." title="Free-text search for subject. Case-insensitive, no special operators supported. Cannot be used with Change ID/SHA1.">
+ </div>
+ </th>
+ <th>
+ <div class="header-wrapper">
+ <span>Repo</span>
+ <input class="header-input" type="text" id="repoFilter" list="repoSuggestions" placeholder="Filter by repository" title="Filter changes by repository name (prefix or full match).">
+ <datalist id="repoSuggestions"></datalist>
+ </div>
+ </th>
+ <th>
+ <div class="header-wrapper">
+ <span>Owner</span>
+ <input class="header-input" type="text" id="ownerFilter" list="ownerSuggestions" placeholder="Filter by owner email" title="Filter changes by owner's email address (full match, select from suggestions).">
+ <datalist id="ownerSuggestions"></datalist>
+ </div>
+ </th>
<th>Merged Date</th>
${Array.from(this.branchColumns).map(b => `
<th>
@@ -1060,6 +1159,80 @@ Gerrit.install(plugin => {
</th>`).join('')}
`;
+ // Get all header inputs and datalists
+ this.subjectSearchInput = this.shadowRoot.getElementById('subjectSearchInput');
+ this.repoFilterInput = this.shadowRoot.getElementById('repoFilter');
+ this.ownerFilterInput = this.shadowRoot.getElementById('ownerFilter');
+ this.onlyMineFilterCheckbox = this.shadowRoot.getElementById('onlyMineFilter');
+ this.repoSuggestionsDatalist = this.shadowRoot.getElementById('repoSuggestions');
+ this.ownerSuggestionsDatalist = this.shadowRoot.getElementById('ownerSuggestions');
+
+ // Set current values from filters
+ if (this.subjectSearchInput) this.subjectSearchInput.value = this.currentFilters.subject || '';
+ if (this.repoFilterInput) this.repoFilterInput.value = this.currentFilters.repo || '';
+ if (this.ownerFilterInput) this.ownerFilterInput.value = this.currentFilters.owner || '';
+
+ // Attach listeners for subject
+ if (this.subjectSearchInput) {
+ this.subjectSearchInput.addEventListener('input', () => {
+ this.updateFilterStates();
+ });
+
+ const applySubjectSearch = () => {
+ const newValue = this.subjectSearchInput.value.trim();
+ // Only apply if value has actually changed
+ if (newValue !== this.currentFilters.subject) {
+ this.applyFilters();
+ }
+ };
+
+ this.subjectSearchInput.addEventListener('keypress', (e) => {
+ if (e.key === 'Enter') {
+ applySubjectSearch();
+ }
+ });
+ this.subjectSearchInput.addEventListener('blur', () => {
+ applySubjectSearch();
+ });
+ }
+
+ // Attach listeners for repo
+ if (this.repoFilterInput) {
+ this.repoFilterInput.addEventListener('input', () => {
+ this.currentFilters.repo = this.repoFilterInput.value;
+ this.debouncedFetchRepos(this.repoFilterInput.value);
+ });
+ this.repoFilterInput.addEventListener('change', () => {
+ this.debouncedFetchRepos.cancel();
+ this.applyFilters();
+ });
+ }
+
+ // Attach listeners for owner
+ if (this.ownerFilterInput) {
+ this.ownerFilterInput.addEventListener('input', () => {
+ this.currentFilters.owner = this.ownerFilterInput.value;
+ if (this.onlyMineFilterCheckbox) {
+ this.onlyMineFilterCheckbox.checked = !!(this.userEmail && this.currentFilters.owner === this.userEmail);
+ }
+ this.debouncedFetchOwners(this.ownerFilterInput.value);
+ });
+ this.ownerFilterInput.addEventListener('change', () => {
+ this.debouncedFetchOwners.cancel();
+ this.applyFilters();
+ });
+ }
+
+ // Attach listener for 'Only Mine' checkbox
+ if (this.onlyMineFilterCheckbox) {
+ this.onlyMineFilterCheckbox.checked = !!(this.userEmail && this.currentFilters.owner === this.userEmail);
+ this.onlyMineFilterCheckbox.addEventListener('change', () => this.handleOnlyMineFilterChange());
+ }
+
+ // Call after inputs are created to set initial disabled states
+ this.updateFilterStates();
+
+
const tbody = this.shadowRoot.querySelector('tbody');
tbody.innerHTML = this.changes
.filter(change => {
@@ -1102,7 +1275,7 @@ Gerrit.install(plugin => {
${Array.from(this.branchColumns).map(b => {
const branch = change.branches[b] || {};
const status = branch.status;
- const overrides = this.getStatusByEnum(status, branch.depends_on);
+ const overrides = getStatusByEnum(status, branch.depends_on);
const text = overrides.textOverride || '-';
const url = branch.url;
const content = url
@@ -1117,14 +1290,14 @@ Gerrit.install(plugin => {
tbody.querySelectorAll('.repo-filter-icon').forEach(icon => {
icon.addEventListener('click', (event) => {
- this.repoFilterInput.value = event.target.dataset.repo;
+ if (this.repoFilterInput) this.repoFilterInput.value = event.target.dataset.repo;
this.currentFilters.repo = event.target.dataset.repo;
this.applyFilters();
});
});
tbody.querySelectorAll('.owner-filter-icon').forEach(icon => {
icon.addEventListener('click', (event) => {
- this.ownerFilterInput.value = event.target.dataset.owner;
+ if (this.ownerFilterInput) this.ownerFilterInput.value = event.target.dataset.owner;
this.currentFilters.owner = event.target.dataset.owner;
this.applyFilters();
});
@@ -1158,4 +1331,127 @@ Gerrit.install(plugin => {
}
customElements.define('cherrypick-status-main', CherryPickMain);
+
+ class CherryPickCommitStatus extends HTMLElement {
+ constructor() {
+ super();
+ this.attachShadow({ mode: 'open' });
+ this.shadowRoot.innerHTML = `
+ <style>
+ h3 {
+ font-family: var(--font-family);
+ font-size: var(--font-size-large, 1em);
+ font-weight: var(--font-weight-bold);
+ line-height: var(--line-height-large);
+ margin: 0 0 0.5rem 0;
+ border-bottom: 1px solid var(--border-color);
+ padding-bottom: 0.25rem;
+ }
+ .status-table {
+ border-collapse: collapse;
+ width: 100%;
+ font-size: var(--font-size-small, 0.9em);
+ }
+ .status-table th, .status-table td {
+ border: 1px solid var(--border-color, #ddd);
+ padding: 0.25rem 0.5rem;
+ text-align: center;
+ }
+ .status-table th {
+ background-color: var(--background-color-tertiary, #f8f9fa);
+ font-weight: var(--font-weight-bold);
+ }
+ .loading, .error {
+ padding: 0.5rem;
+ color: var(--deemphasized-text-color);
+ }
+ .error {
+ color: var(--error-text-color);
+ }
+ a {
+ color: currentColor;
+ text-decoration: none;
+ }
+ a:hover {
+ text-decoration: underline;
+ }
+ </style>
+ <div id="container"></div>
+ `;
+ this.container = this.shadowRoot.getElementById('container');
+ }
+
+ setData(element) {
+ const commitMessage = element.revision.commit.message;
+ if (commitMessage && commitMessage.includes('Pick-to:')) {
+ const changeId = element.change.triplet_id || element.change.id;
+ this.fetchStatus(changeId);
+ }
+ }
+
+ async fetchStatus(changeId) {
+ this.container.innerHTML = `<div class="loading">Loading cherry-pick status...</div>`;
+ try {
+ const response = await fetch(`/config/server/getdashboard?changeid=${changeId}`);
+ if (!response.ok) {
+ throw new Error(`HTTP error! status: ${response.status}`);
+ }
+ const data = await response.json();
+
+ if (data.changes && data.changes.length > 0) {
+ this.render(data.changes[0]);
+ } else {
+ this.container.innerHTML = ''; // No data found, render nothing.
+ }
+ } catch (err) {
+ console.error('Error fetching cherry-pick status:', err);
+ this.container.innerHTML = `<div class="error">Error loading cherry-pick status.</div>`;
+ }
+ }
+
+ render(change) {
+ const branches = change.branches || {};
+ const branchNames = sortBranchNames(Object.keys(branches));
+
+ if (branchNames.length === 0) {
+ this.container.innerHTML = ''; // No branches to show
+ return;
+ }
+
+ const tableHeader = branchNames.map(b => `<th>${b}</th>`).join('');
+ const tableBody = branchNames.map(b => {
+ const branchInfo = branches[b] || {};
+ const status = branchInfo.status;
+ const overrides = getStatusByEnum(status, branchInfo.depends_on);
+ const text = overrides.textOverride || '-';
+ const url = branchInfo.url;
+ const content = url ? `<a href="http://code.qt.io/$%7Burl%7D" target="_blank" title="View change for ${b}">${text}</a>` : text;
+ return `<td style="background-color: ${overrides.bg}; color: ${overrides.text}">${content}</td>`;
+ }).join('');
+
+ this.container.innerHTML = `
+ <h3>Cherry-Pick Status</h3>
+ <table class="status-table">
+ <thead>
+ <tr>${tableHeader}</tr>
+ </thead>
+ <tbody>
+ <tr>${tableBody}</tr>
+ </tbody>
+ </table>
+ `;
+ }
+ }
+
+ customElements.define('cherrypick-commit-status', CherryPickCommitStatus);
+
+ plugin.hook('commit-container').onAttached(element => {
+ // The hook can be called multiple times, so check if element already exists.
+ if (!isLoggedIn || element.content.querySelector('cherrypick-commit-status')) {
+ return;
+ }
+ const statusElement = document.createElement('cherrypick-commit-status');
+ statusElement.setData(element);
+ element.appendChild(statusElement);
+ });
});