Merge "Handle checks summary chip clicks by filtering the tab accordingly"
diff --git a/Documentation/js_licenses.txt b/Documentation/js_licenses.txt
index 48e729f..7ef9473 100644
--- a/Documentation/js_licenses.txt
+++ b/Documentation/js_licenses.txt
@@ -247,6 +247,38 @@
----
+[[DefinitelyTyped]]
+DefinitelyTyped
+
+* @types/resize-observer-browser
+
+[[DefinitelyTyped_license]]
+----
+ MIT License
+
+ Copyright (c) Microsoft Corporation.
+
+ Permission is hereby granted, free of charge, to any person obtaining a copy
+ of this software and associated documentation files (the "Software"), to deal
+ in the Software without restriction, including without limitation the rights
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ copies of the Software, and to permit persons to whom the Software is
+ furnished to do so, subject to the following conditions:
+
+ The above copyright notice and this permission notice shall be included in all
+ copies or substantial portions of the Software.
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ SOFTWARE
+
+----
+
+
[[Polymer-2014]]
Polymer-2014
diff --git a/Documentation/licenses.txt b/Documentation/licenses.txt
index b7cdf8a..5f0fb65 100644
--- a/Documentation/licenses.txt
+++ b/Documentation/licenses.txt
@@ -3204,6 +3204,38 @@
----
+[[DefinitelyTyped]]
+DefinitelyTyped
+
+* @types/resize-observer-browser
+
+[[DefinitelyTyped_license]]
+----
+ MIT License
+
+ Copyright (c) Microsoft Corporation.
+
+ Permission is hereby granted, free of charge, to any person obtaining a copy
+ of this software and associated documentation files (the "Software"), to deal
+ in the Software without restriction, including without limitation the rights
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ copies of the Software, and to permit persons to whom the Software is
+ furnished to do so, subject to the following conditions:
+
+ The above copyright notice and this permission notice shall be included in all
+ copies or substantial portions of the Software.
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ SOFTWARE
+
+----
+
+
[[Polymer-2014]]
Polymer-2014
diff --git a/Documentation/rest-api-config.txt b/Documentation/rest-api-config.txt
index 9764c8a..a8d9b3d 100644
--- a/Documentation/rest-api-config.txt
+++ b/Documentation/rest-api-config.txt
@@ -1839,6 +1839,8 @@
Whether to enable the web UI for editing GPG keys.
|`report_bug_url` |optional|
link:config-gerrit.html#gerrit.reportBugUrl[URL to report bugs].
+|`instance_id` |optional|
+link:config-gerrit.html#gerrit.instanceId[Short identifier for this Gerrit installation].
|=================================
[[index-config-info]]
diff --git a/Documentation/user-search.txt b/Documentation/user-search.txt
index e02dc21..52c282e 100644
--- a/Documentation/user-search.txt
+++ b/Documentation/user-search.txt
@@ -190,6 +190,13 @@
+
Changes occurring in projects starting with 'PREFIX'.
+[[parentof]]
+parentof:'ID'::
+Changes which are parent to the change specified by 'ID'. Change 'ID' can be
+specified as a legacy numerical 'ID' such as 15183, or a Change-Id that can be
+picked from the commit message. This operator will return immediate parents
+and will not return grand parents or higher level ancestors of the given change.
+
[[parentproject]]
parentproject:'PROJECT'::
+
diff --git a/java/com/google/gerrit/extensions/common/GerritInfo.java b/java/com/google/gerrit/extensions/common/GerritInfo.java
index 2ae6703..3265a00 100644
--- a/java/com/google/gerrit/extensions/common/GerritInfo.java
+++ b/java/com/google/gerrit/extensions/common/GerritInfo.java
@@ -23,4 +23,5 @@
public Boolean editGpgKeys;
public String reportBugUrl;
public String primaryWeblinkName;
+ public String instanceId;
}
diff --git a/java/com/google/gerrit/server/patch/AutoMerger.java b/java/com/google/gerrit/server/patch/AutoMerger.java
index fe915c5..ac37411 100644
--- a/java/com/google/gerrit/server/patch/AutoMerger.java
+++ b/java/com/google/gerrit/server/patch/AutoMerger.java
@@ -109,6 +109,7 @@
ActionType.GIT_UPDATE,
"createAutoMerge",
() -> createAutoMergeCommit(repo, rw, ins, merge, mergeStrategy))
+ .defaultTimeoutMultiplier(2)
.call();
} catch (Exception e) {
Throwables.throwIfUnchecked(e);
diff --git a/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java b/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java
index 68a90d2..4e3edcd 100644
--- a/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java
+++ b/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java
@@ -173,6 +173,7 @@
public static final String FIELD_MESSAGE = "message";
public static final String FIELD_OWNER = "owner";
public static final String FIELD_OWNERIN = "ownerin";
+ public static final String FIELD_PARENTOF = "parentof";
public static final String FIELD_PARENTPROJECT = "parentproject";
public static final String FIELD_PATH = "path";
public static final String FIELD_PENDING_REVIEWER = "pendingreviewer";
@@ -735,6 +736,16 @@
}
@Operator
+ public Predicate<ChangeData> parentof(String value) throws QueryParseException {
+ List<ChangeData> changes = parseChangeData(value);
+ List<Predicate<ChangeData>> or = new ArrayList<>(changes.size());
+ for (ChangeData c : changes) {
+ or.add(new ParentOfPredicate(value, c, args.repoManager));
+ }
+ return Predicate.or(or);
+ }
+
+ @Operator
public Predicate<ChangeData> parentproject(String name) {
return new ParentProjectPredicate(args.projectCache, args.childProjects, name);
}
@@ -1560,14 +1571,18 @@
}
private List<Change> parseChange(String value) throws QueryParseException {
+ return asChanges(parseChangeData(value));
+ }
+
+ private List<ChangeData> parseChangeData(String value) throws QueryParseException {
if (PAT_LEGACY_ID.matcher(value).matches()) {
Optional<Change.Id> id = Change.Id.tryParse(value);
if (!id.isPresent()) {
throw error("Invalid change id " + value);
}
- return asChanges(args.queryProvider.get().byLegacyChangeId(id.get()));
+ return args.queryProvider.get().byLegacyChangeId(id.get());
} else if (PAT_CHANGE_ID.matcher(value).matches()) {
- List<Change> changes = asChanges(args.queryProvider.get().byKeyPrefix(parseChangeId(value)));
+ List<ChangeData> changes = args.queryProvider.get().byKeyPrefix(parseChangeId(value));
if (changes.isEmpty()) {
throw error("Change " + value + " not found");
}
diff --git a/java/com/google/gerrit/server/query/change/ParentOfPredicate.java b/java/com/google/gerrit/server/query/change/ParentOfPredicate.java
new file mode 100644
index 0000000..e48d586
--- /dev/null
+++ b/java/com/google/gerrit/server/query/change/ParentOfPredicate.java
@@ -0,0 +1,62 @@
+// Copyright (C) 2021 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.query.change;
+
+import com.google.common.collect.Sets;
+import com.google.gerrit.entities.PatchSet;
+import com.google.gerrit.exceptions.StorageException;
+import com.google.gerrit.index.query.Matchable;
+import com.google.gerrit.index.query.OperatorPredicate;
+import com.google.gerrit.server.git.GitRepositoryManager;
+import java.io.IOException;
+import java.util.Set;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.eclipse.jgit.revwalk.RevWalk;
+
+public class ParentOfPredicate extends OperatorPredicate<ChangeData>
+ implements Matchable<ChangeData> {
+ protected final Set<RevCommit> parents;
+
+ public ParentOfPredicate(String value, ChangeData change, GitRepositoryManager repoManager) {
+ super(ChangeQueryBuilder.FIELD_PARENTOF, value);
+ this.parents = getParents(change, repoManager);
+ }
+
+ @Override
+ public boolean match(ChangeData changeData) {
+ return changeData.patchSets().stream().anyMatch(ps -> parents.contains(ps.commitId()));
+ }
+
+ @Override
+ public int getCost() {
+ return 1;
+ }
+
+ protected Set<RevCommit> getParents(ChangeData change, GitRepositoryManager repoManager) {
+ PatchSet ps = change.currentPatchSet();
+ try (Repository repo = repoManager.openRepository(change.project());
+ RevWalk walk = new RevWalk(repo)) {
+ RevCommit c = walk.parseCommit(ps.commitId());
+ return Sets.newHashSet(c.getParents());
+ } catch (IOException e) {
+ throw new StorageException(
+ String.format(
+ "Loading commit %s for ps %d of change %d failed.",
+ ps.commitId(), ps.id().get(), ps.id().changeId().get()),
+ e);
+ }
+ }
+}
diff --git a/java/com/google/gerrit/server/restapi/change/PostReview.java b/java/com/google/gerrit/server/restapi/change/PostReview.java
index 562bdf8..73b38b2 100644
--- a/java/com/google/gerrit/server/restapi/change/PostReview.java
+++ b/java/com/google/gerrit/server/restapi/change/PostReview.java
@@ -1228,9 +1228,10 @@
}
private boolean isReviewer(ChangeContext ctx) {
- ChangeData cd = changeDataFactory.create(ctx.getNotes());
- ReviewerSet reviewers = cd.reviewers();
- return reviewers.byState(REVIEWER).contains(ctx.getAccountId());
+ return approvalsUtil
+ .getReviewers(ctx.getNotes())
+ .byState(REVIEWER)
+ .contains(ctx.getAccountId());
}
private boolean updateLabels(ProjectState projectState, ChangeContext ctx)
diff --git a/java/com/google/gerrit/server/restapi/config/GetServerInfo.java b/java/com/google/gerrit/server/restapi/config/GetServerInfo.java
index 5459ede..0a5692e 100644
--- a/java/com/google/gerrit/server/restapi/config/GetServerInfo.java
+++ b/java/com/google/gerrit/server/restapi/config/GetServerInfo.java
@@ -304,6 +304,7 @@
info.editGpgKeys =
toBoolean(enableSignedPush && config.getBoolean("gerrit", null, "editGpgKeys", true));
info.primaryWeblinkName = config.getString("gerrit", null, "primaryWeblinkName");
+ info.instanceId = config.getString("gerrit", null, "instanceId");
return info;
}
diff --git a/java/com/google/gerrit/server/submit/MergeOp.java b/java/com/google/gerrit/server/submit/MergeOp.java
index 871d8d2..4e732a4 100644
--- a/java/com/google/gerrit/server/submit/MergeOp.java
+++ b/java/com/google/gerrit/server/submit/MergeOp.java
@@ -549,8 +549,8 @@
.listener(retryTracker)
// Up to the entire submit operation is retried, including possibly many projects.
// Multiply the timeout by the number of projects we're actually attempting to
- // submit.
- .defaultTimeoutMultiplier(filteredNoteDbChangeSet.projects().size())
+ // submit. Times 2 to retry more persistently, to increase success rate.
+ .defaultTimeoutMultiplier(filteredNoteDbChangeSet.projects().size() * 2)
// By default, we only retry lock failures. Here it's better to also retry unexpected
// runtime exceptions.
.retryOn(t -> t instanceof RuntimeException)
diff --git a/javatests/com/google/gerrit/acceptance/rest/config/ServerInfoIT.java b/javatests/com/google/gerrit/acceptance/rest/config/ServerInfoIT.java
index 0a84db4..4738f64 100644
--- a/javatests/com/google/gerrit/acceptance/rest/config/ServerInfoIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/config/ServerInfoIT.java
@@ -74,6 +74,7 @@
@GerritConfig(name = "gerrit.allProjects", value = "Root")
@GerritConfig(name = "gerrit.allUsers", value = "Users")
@GerritConfig(name = "gerrit.reportBugUrl", value = "https://example.com/report")
+ @GerritConfig(name = "gerrit.instanceId", value = "devops-instance")
// suggest
@GerritConfig(name = "suggest.from", value = "3")
@@ -116,6 +117,7 @@
assertThat(i.gerrit.allProjects).isEqualTo("Root");
assertThat(i.gerrit.allUsers).isEqualTo("Users");
assertThat(i.gerrit.reportBugUrl).isEqualTo("https://example.com/report");
+ assertThat(i.gerrit.instanceId).isEqualTo("devops-instance");
// plugin
assertThat(i.plugin.jsResourcePaths).isEmpty();
@@ -184,6 +186,7 @@
assertThat(i.gerrit.allProjects).isEqualTo(AllProjectsNameProvider.DEFAULT);
assertThat(i.gerrit.allUsers).isEqualTo(AllUsersNameProvider.DEFAULT);
assertThat(i.gerrit.reportBugUrl).isNull();
+ assertThat(i.gerrit.instanceId).isNull();
// plugin
assertThat(i.plugin.jsResourcePaths).isEmpty();
diff --git a/javatests/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java b/javatests/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java
index 7efcb4b..48bd321 100644
--- a/javatests/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java
+++ b/javatests/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java
@@ -705,6 +705,24 @@
}
@Test
+ public void byParentOf() throws Exception {
+ TestRepository<Repo> repo1 = createProject("repo1");
+ RevCommit commit1 = repo1.parseBody(repo1.commit().message("message").create());
+ Change change1 = insert(repo1, newChangeForCommit(repo1, commit1));
+ RevCommit commit2 = repo1.parseBody(repo1.commit(commit1));
+ Change change2 = insert(repo1, newChangeForCommit(repo1, commit2));
+ RevCommit commit3 = repo1.parseBody(repo1.commit(commit1, commit2));
+ Change change3 = insert(repo1, newChangeForCommit(repo1, commit3));
+
+ assertQuery("parentof:" + change1.getId().get());
+ assertQuery("parentof:" + change1.getKey().get());
+ assertQuery("parentof:" + change2.getId().get(), change1);
+ assertQuery("parentof:" + change2.getKey().get(), change1);
+ assertQuery("parentof:" + change3.getId().get(), change2, change1);
+ assertQuery("parentof:" + change3.getKey().get(), change2, change1);
+ }
+
+ @Test
public void byParentProject() throws Exception {
TestRepository<Repo> repo1 = createProject("repo1");
TestRepository<Repo> repo2 = createProject("repo2", "repo1");
diff --git a/modules/jgit b/modules/jgit
index 4560bdf..9bfb0f3 160000
--- a/modules/jgit
+++ b/modules/jgit
@@ -1 +1 @@
-Subproject commit 4560bdf7e2e3c16a7c7bb3f2fcf067bb1eee26fb
+Subproject commit 9bfb0f3a4ec856dcbebb477a1ee8803a3c47c194
diff --git a/polygerrit-ui/app/elements/change-list/gr-create-change-help/gr-create-change-help_html.ts b/polygerrit-ui/app/elements/change-list/gr-create-change-help/gr-create-change-help_html.ts
index 9812933..add7ca5 100644
--- a/polygerrit-ui/app/elements/change-list/gr-create-change-help/gr-create-change-help_html.ts
+++ b/polygerrit-ui/app/elements/change-list/gr-create-change-help/gr-create-change-help_html.ts
@@ -36,7 +36,7 @@
width: 10em;
}
#graphic iron-icon {
- color: #9e9e9e;
+ color: var(--gray-foreground);
height: 5em;
width: 5em;
}
diff --git a/polygerrit-ui/app/elements/change-list/gr-dashboard-view/gr-dashboard-view.ts b/polygerrit-ui/app/elements/change-list/gr-dashboard-view/gr-dashboard-view.ts
index 9240905..816c8ef 100644
--- a/polygerrit-ui/app/elements/change-list/gr-dashboard-view/gr-dashboard-view.ts
+++ b/polygerrit-ui/app/elements/change-list/gr-dashboard-view/gr-dashboard-view.ts
@@ -58,7 +58,7 @@
import {firePageError, fireTitleChange} from '../../../utils/event-util';
import {GerritView} from '../../../services/router/router-model';
-const PROJECT_PLACEHOLDER_PATTERN = /\$\{project\}/g;
+const PROJECT_PLACEHOLDER_PATTERN = /\${project}/g;
const RELOAD_DASHBOARD_INTERVAL_MS = 10 * 1000;
export interface GrDashboardView {
diff --git a/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata_html.ts b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata_html.ts
index c5c73c5..59bf8ad 100644
--- a/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata_html.ts
+++ b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata_html.ts
@@ -78,7 +78,7 @@
}
.icon.help,
.icon.notTrusted {
- color: #ffa62f;
+ color: var(--warning-foreground);
}
.icon.invalid {
color: var(--negative-red-text-color);
@@ -87,7 +87,7 @@
color: var(--positive-green-text-color);
}
.parentList.notCurrent.nonMerge #parentNotCurrentMessage {
- --arrow-color: #ffa62f;
+ --arrow-color: var(--warning-foreground);
display: inline-block;
}
.separatedSection {
diff --git a/polygerrit-ui/app/elements/change/gr-change-requirements/gr-change-requirements.ts b/polygerrit-ui/app/elements/change/gr-change-requirements/gr-change-requirements.ts
index c0e87f3..adc7fd3 100644
--- a/polygerrit-ui/app/elements/change/gr-change-requirements/gr-change-requirements.ts
+++ b/polygerrit-ui/app/elements/change/gr-change-requirements/gr-change-requirements.ts
@@ -37,6 +37,7 @@
import {PolymerDeepPropertyChange} from '@polymer/polymer/interfaces';
import {appContext} from '../../../services/app-context';
import {KnownExperimentId} from '../../../services/flags/flags';
+import {labelCompare} from '../../../utils/label-util';
interface ChangeRequirement extends Requirement {
satisfied: boolean;
@@ -136,7 +137,7 @@
const labels = labelsRecord.base || {};
const allLabels: Label[] = [];
- for (const label of Object.keys(labels).sort()) {
+ for (const label of Object.keys(labels).sort(labelCompare)) {
allLabels.push({
labelName: label,
icon: this._computeLabelIcon(labels[label]),
diff --git a/polygerrit-ui/app/elements/change/gr-change-requirements/gr-change-requirements_html.ts b/polygerrit-ui/app/elements/change/gr-change-requirements/gr-change-requirements_html.ts
index e02b337..a502949 100644
--- a/polygerrit-ui/app/elements/change/gr-change-requirements/gr-change-requirements_html.ts
+++ b/polygerrit-ui/app/elements/change/gr-change-requirements/gr-change-requirements_html.ts
@@ -23,7 +23,7 @@
width: 100%;
}
.status {
- color: #ffa62f;
+ color: var(--warning-foreground);
display: inline-block;
text-align: center;
vertical-align: top;
diff --git a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_html.ts b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_html.ts
index 23718fa..c57a2d5 100644
--- a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_html.ts
+++ b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_html.ts
@@ -154,7 +154,7 @@
padding-left: var(--spacing-s);
}
.drafts {
- color: #c62828;
+ color: var(--error-foreground);
font-weight: var(--font-weight-bold);
}
.show-hide-icon:focus {
diff --git a/polygerrit-ui/app/elements/change/gr-label-scores/gr-label-scores.ts b/polygerrit-ui/app/elements/change/gr-label-scores/gr-label-scores.ts
index a966186..661cd1a 100644
--- a/polygerrit-ui/app/elements/change/gr-label-scores/gr-label-scores.ts
+++ b/polygerrit-ui/app/elements/change/gr-label-scores/gr-label-scores.ts
@@ -37,6 +37,7 @@
} from '../gr-label-score-row/gr-label-score-row';
import {PolymerDeepPropertyChange} from '@polymer/polymer/interfaces';
import {appContext} from '../../../services/app-context';
+import {labelCompare} from '../../../utils/label-util';
@customElement('gr-label-scores')
export class GrLabelScores extends GestureEventListeners(
@@ -147,7 +148,7 @@
if (!labelRecord?.base) return [];
const labelsObj = labelRecord.base;
return Object.keys(labelsObj)
- .sort()
+ .sort(labelCompare)
.map(key => {
return {
name: key,
diff --git a/polygerrit-ui/app/elements/change/gr-message/gr-message.ts b/polygerrit-ui/app/elements/change/gr-message/gr-message.ts
index ce17b1e..f913459 100644
--- a/polygerrit-ui/app/elements/change/gr-message/gr-message.ts
+++ b/polygerrit-ui/app/elements/change/gr-message/gr-message.ts
@@ -54,7 +54,7 @@
} from '../../../utils/patch-set-util';
import {isServiceUser} from '../../../utils/account-util';
-const PATCH_SET_PREFIX_PATTERN = /^(?:Uploaded\s*)?(?:P|p)atch (?:S|s)et \d+:\s*(.*)/;
+const PATCH_SET_PREFIX_PATTERN = /^(?:Uploaded\s*)?[Pp]atch [Ss]et \d+:\s*(.*)/;
const LABEL_TITLE_SCORE_PATTERN = /^(-?)([A-Za-z0-9-]+?)([+-]\d+)?[.]?$/;
const UPLOADED_NEW_PATCHSET_PATTERN = /Uploaded patch set (\d+)./;
const MERGED_PATCHSET_PATTERN = /(\d+) is the latest approved patch-set/;
diff --git a/polygerrit-ui/app/elements/change/gr-related-changes-list-experimental/gr-related-change.ts b/polygerrit-ui/app/elements/change/gr-related-changes-list-experimental/gr-related-change.ts
index 3ed545e..7b698f1 100644
--- a/polygerrit-ui/app/elements/change/gr-related-changes-list-experimental/gr-related-change.ts
+++ b/polygerrit-ui/app/elements/change/gr-related-changes-list-experimental/gr-related-change.ts
@@ -77,10 +77,10 @@
margin-left: var(--spacing-xs);
}
.notCurrent {
- color: #e65100;
+ color: var(--warning-foreground);
}
.indirectAncestor {
- color: #33691e;
+ color: var(--indirect-ancestor-text-color);
}
.submittableCheck {
padding-left: var(--spacing-s);
diff --git a/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-changes-list_html.ts b/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-changes-list_html.ts
index 9941fa9..2f53319 100644
--- a/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-changes-list_html.ts
+++ b/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-changes-list_html.ts
@@ -66,10 +66,10 @@
margin-left: var(--spacing-xs);
}
.notCurrent {
- color: #e65100;
+ color: var(--warning-foreground);
}
.indirectAncestor {
- color: #33691e;
+ color: var(--indirect-ancestor-text-color);
}
.submittableCheck {
padding-left: var(--spacing-s);
diff --git a/polygerrit-ui/app/elements/checks/gr-checks-results.ts b/polygerrit-ui/app/elements/checks/gr-checks-results.ts
index e448374..002c8c3 100644
--- a/polygerrit-ui/app/elements/checks/gr-checks-results.ts
+++ b/polygerrit-ui/app/elements/checks/gr-checks-results.ts
@@ -295,6 +295,8 @@
@property()
runs: CheckRun[] = [];
+ private isSectionExpanded = new Map<Category | 'SUCCESS', boolean>();
+
static get styles() {
return [
sharedStyles,
@@ -312,23 +314,36 @@
margin-top: var(--spacing-l);
margin-left: var(--spacing-l);
text-transform: capitalize;
+ cursor: default;
}
- .categoryHeader iron-icon {
+ .categoryHeader .expandIcon {
+ width: var(--line-height-h3);
+ height: var(--line-height-h3);
+ margin-right: var(--spacing-s);
+ }
+ .categoryHeader .statusIcon {
position: relative;
- top: 1px;
+ top: 2px;
}
- .categoryHeader iron-icon.error {
+ .categoryHeader .statusIcon.error {
color: var(--error-foreground);
}
- .categoryHeader iron-icon.warning {
+ .categoryHeader .statusIcon.warning {
color: var(--warning-foreground);
}
- .categoryHeader iron-icon.info {
+ .categoryHeader .statusIcon.info {
color: var(--info-foreground);
}
- .categoryHeader iron-icon.success {
+ .categoryHeader .statusIcon.success {
color: var(--success-foreground);
}
+ .collapsed table {
+ display: none;
+ }
+ .collapsed {
+ border-bottom: 1px solid var(--border-color);
+ padding-bottom: var(--spacing-m);
+ }
.noCompleted {
margin-top: var(--spacing-l);
}
@@ -354,7 +369,7 @@
${this.renderFilter()} ${this.renderNoCompleted()}
${this.renderSection(Category.ERROR)}
${this.renderSection(Category.WARNING)}
- ${this.renderSection(Category.INFO)} ${this.renderSuccess()}
+ ${this.renderSection(Category.INFO)} ${this.renderSection('SUCCESS')}
`;
}
@@ -384,36 +399,62 @@
return html`<div class="noCompleted">${text}</div>`;
}
- renderSection(category: Category) {
+ renderSection(category: Category | 'SUCCESS') {
const catString = category.toString().toLowerCase();
- const runs = this.runs.filter(r =>
- (r.results ?? []).some(res => res.category === category)
- );
+ let runs = this.runs;
+ if (category === 'SUCCESS') {
+ runs = runs
+ .filter(hasCompletedWithoutResults)
+ .filter(r => this.filterRegExp.test(r.checkName));
+ } else {
+ runs = runs.filter(r =>
+ (r.results ?? []).some(res => res.category === category)
+ );
+ }
if (runs.length === 0) return;
+ const expanded = this.isSectionExpanded.get(category) ?? true;
+ const expandedClass = expanded ? 'expanded' : 'collapsed';
+ const icon = expanded ? 'gr-icons:expand-more' : 'gr-icons:expand-less';
return html`
- <h3 class="categoryHeader heading-3">
- <iron-icon
- icon="gr-icons:${iconForCategory(category)}"
- class="${catString}"
- ></iron-icon>
- ${catString}
- </h3>
- <table class="resultsTable">
- <thead>
- <tr class="headerRow">
- <th class="iconCol"></th>
- <th class="nameCol">Run</th>
- <th class="summaryCol">Summary</th>
- <th class="expanderCol"></th>
- </tr>
- </thead>
- <tbody>
- ${runs.map(run => this.renderRun(category, run))}
- </tbody>
- </table>
+ <div class="${expandedClass}">
+ <h3
+ class="categoryHeader heading-3"
+ @click="${() => this.toggleExpanded(category)}"
+ >
+ <iron-icon class="expandIcon" icon="${icon}"></iron-icon>
+ <iron-icon
+ icon="gr-icons:${iconForCategory(category)}"
+ class="statusIcon ${catString}"
+ ></iron-icon>
+ ${catString}
+ </h3>
+ <table class="resultsTable">
+ <thead>
+ <tr class="headerRow">
+ <th class="iconCol"></th>
+ <th class="nameCol">Run</th>
+ <th class="summaryCol">Summary</th>
+ <th class="expanderCol"></th>
+ </tr>
+ </thead>
+ <tbody>
+ ${runs.map(run =>
+ category === 'SUCCESS'
+ ? this.renderSuccessfulRun(run)
+ : this.renderRun(category, run)
+ )}
+ </tbody>
+ </table>
+ </div>
`;
}
+ toggleExpanded(category: Category | 'SUCCESS') {
+ const expanded = this.isSectionExpanded.get(category) ?? true;
+ this.isSectionExpanded.set(category, !expanded);
+ this.requestUpdate();
+ }
+
renderRun(category: Category, run: CheckRun) {
return html`${run.results
?.filter(result => result.category === category)
@@ -428,31 +469,6 @@
)}`;
}
- renderSuccess() {
- const runs = this.runs
- .filter(hasCompletedWithoutResults)
- .filter(r => this.filterRegExp.test(r.checkName));
- if (runs.length === 0) return;
- return html`
- <h3 class="categoryHeader heading-3">
- <iron-icon
- icon="gr-icons:check-circle-outline"
- class="success"
- ></iron-icon>
- Success
- </h3>
- <table class="resultsTable">
- <tr class="headerRow">
- <th class="iconCol"></th>
- <th class="nameCol">Run</th>
- <th class="summaryCol">Summary</th>
- <th class="expanderCol"></th>
- </tr>
- ${runs.map(run => this.renderSuccessfulRun(run))}
- </table>
- `;
- }
-
renderSuccessfulRun(run: CheckRun) {
const adaptedRun: RunResult = {
category: Category.INFO, // will not be used, but is required
diff --git a/polygerrit-ui/app/elements/checks/gr-checks-runs.ts b/polygerrit-ui/app/elements/checks/gr-checks-runs.ts
index 3e0a39b..1b49f8a 100644
--- a/polygerrit-ui/app/elements/checks/gr-checks-runs.ts
+++ b/polygerrit-ui/app/elements/checks/gr-checks-runs.ts
@@ -111,6 +111,9 @@
.chip.placeholder {
border-left: var(--thick-border) solid var(--border-color);
}
+ .chip.placeholder iron-icon {
+ display: none;
+ }
.chip.error iron-icon {
color: var(--error-foreground);
}
@@ -239,6 +242,8 @@
@property()
selectedRuns: string[] = [];
+ private isSectionExpanded = new Map<RunStatus, boolean>();
+
constructor() {
super();
this.subscribe('runs', allRuns$);
@@ -252,9 +257,24 @@
display: block;
padding: var(--spacing-xl);
}
- .statusHeader {
+ .expandIcon {
+ width: var(--line-height-h3);
+ height: var(--line-height-h3);
+ }
+ .sectionHeader {
padding-top: var(--spacing-l);
text-transform: capitalize;
+ cursor: default;
+ }
+ .sectionHeader h3 {
+ display: inline-block;
+ }
+ .collapsed .sectionRuns {
+ display: none;
+ }
+ .collapsed {
+ border-bottom: 1px solid var(--border-color);
+ padding-bottom: var(--spacing-m);
}
input#filterInput {
margin-top: var(--spacing-s);
@@ -345,14 +365,31 @@
.filter(r => this.filterRegExp.test(r.checkName))
.sort(compareByWorstCategory);
if (runs.length === 0) return;
+ const expanded = this.isSectionExpanded.get(status) ?? true;
+ const expandedClass = expanded ? 'expanded' : 'collapsed';
+ const icon = expanded ? 'gr-icons:expand-more' : 'gr-icons:expand-less';
return html`
- <div class="${status.toLowerCase()}">
- <h3 class="statusHeader heading-3">${status.toLowerCase()}</h3>
- ${runs.map(run => this.renderRun(run))}
+ <div class="${status.toLowerCase()} ${expandedClass}">
+ <div
+ class="sectionHeader"
+ @click="${() => this.toggleExpanded(status)}"
+ >
+ <iron-icon class="expandIcon" icon="${icon}"></iron-icon>
+ <h3 class="heading-3">${status.toLowerCase()}</h3>
+ </div>
+ <div class="sectionRuns">
+ ${runs.map(run => this.renderRun(run))}
+ </div>
</div>
`;
}
+ toggleExpanded(status: RunStatus) {
+ const expanded = this.isSectionExpanded.get(status) ?? true;
+ this.isSectionExpanded.set(status, !expanded);
+ this.requestUpdate();
+ }
+
renderRun(run: CheckRun) {
const selected = this.selectedRuns.includes(run.checkName);
const deselected = !selected && this.selectedRuns.length > 0;
diff --git a/polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar.ts b/polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar.ts
index 43f8a2b..97d5271 100644
--- a/polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar.ts
+++ b/polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar.ts
@@ -93,6 +93,7 @@
'onlyextensions:',
'owner:',
'ownerin:',
+ 'parentof:',
'parentproject:',
'project:',
'projects:',
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-image-viewer/gr-overview-image.ts b/polygerrit-ui/app/elements/diff/gr-diff-image-viewer/gr-overview-image.ts
new file mode 100644
index 0000000..42268e9
--- /dev/null
+++ b/polygerrit-ui/app/elements/diff/gr-diff-image-viewer/gr-overview-image.ts
@@ -0,0 +1,304 @@
+/**
+ * @license
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import {
+ css,
+ customElement,
+ html,
+ internalProperty,
+ LitElement,
+ property,
+ PropertyValues,
+ query,
+} from 'lit-element';
+import {StyleInfo, styleMap} from 'lit-html/directives/style-map';
+
+import {Dimensions, fitToFrame, Point, Rect} from './util';
+
+/**
+ * Displays a scaled-down version of an image with a draggable frame for
+ * choosing a portion of the image to be magnified by other components.
+ *
+ * Slotted content can be arbitrary elements, but should be limited to images or
+ * stacks of image-like elements (e.g. for overlays) with limited interactivity,
+ * to prevent confusion, as the component only captures a limited set of events.
+ * Slotted content is scaled to fit the bounds of the component, with
+ * letterboxing if aspect ratios differ. For slotted content smaller than the
+ * component, it will cap the scale at 1x and also apply letterboxing.
+ */
+@customElement('gr-overview-image')
+export class GrOverviewImage extends LitElement {
+ @property({type: Object})
+ frameRect: Rect = {origin: {x: 0, y: 0}, dimensions: {width: 0, height: 0}};
+
+ @internalProperty() protected contentStyle: StyleInfo = {};
+
+ @internalProperty() protected contentTransformStyle: StyleInfo = {};
+
+ @internalProperty() protected frameStyle: StyleInfo = {};
+
+ @internalProperty() protected overlayStyle: StyleInfo = {};
+
+ @internalProperty() protected dragging = false;
+
+ @query('.content-box') protected contentBox!: HTMLDivElement;
+
+ @query('.content') protected content!: HTMLDivElement;
+
+ @query('.content-transform') protected contentTransform!: HTMLDivElement;
+
+ @query('.frame') protected frame!: HTMLDivElement;
+
+ private contentBounds: Dimensions = {width: 0, height: 0};
+
+ private imageBounds: Dimensions = {width: 0, height: 0};
+
+ private scale = 1;
+
+ // When grabbing the frame to drag it around, this stores the offset of the
+ // cursor from the center of the frame at the start of the drag.
+ private grabOffset: Point = {x: 0, y: 0};
+
+ private readonly resizeObserver = new ResizeObserver(
+ (entries: ResizeObserverEntry[]) => {
+ for (const entry of entries) {
+ if (entry.target === this.contentBox) {
+ this.contentBounds = {
+ width: entry.contentRect.width,
+ height: entry.contentRect.height,
+ };
+ }
+ if (entry.target === this.contentTransform) {
+ this.imageBounds = {
+ width: entry.contentRect.width,
+ height: entry.contentRect.height,
+ };
+ }
+ this.updateScale();
+ }
+ }
+ );
+
+ static styles = css`
+ :host {
+ --overview-image-background-color: #000;
+ --overview-image-frame-color: #f00;
+ display: flex;
+ }
+ * {
+ box-sizing: border-box;
+ }
+ ::slotted(*) {
+ display: block;
+ }
+ .content-box {
+ border: 1px solid var(--overview-image-background-color);
+ background-color: var(--overview-iamge-background-color);
+ width: 100%;
+ position: relative;
+ }
+ .content {
+ position: absolute;
+ cursor: pointer;
+ }
+ .content-transform {
+ position: absolute;
+ transform-origin: top left;
+ will-change: transform;
+ }
+ .frame {
+ border: 1px solid var(--overview-image-frame-color);
+ position: absolute;
+ will-change: transform;
+ }
+ .overlay {
+ position: absolute;
+ z-index: 10000;
+ cursor: grabbing;
+ }
+ `;
+
+ render() {
+ return html`
+ <div class="content-box">
+ <div
+ class="content"
+ style="${styleMap({
+ ...this.contentStyle,
+ })}"
+ @mousemove="${this.maybeDragFrame}"
+ @mousedown=${this.clickOverview}
+ @mouseup="${this.releaseFrame}"
+ >
+ <div
+ class="content-transform"
+ style="${styleMap(this.contentTransformStyle)}"
+ >
+ <slot></slot>
+ </div>
+ <div
+ class="frame"
+ style="${styleMap({
+ ...this.frameStyle,
+ cursor: this.dragging ? 'grabbing' : 'grab',
+ })}"
+ @mousedown="${this.grabFrame}"
+ ></div>
+ </div>
+ <div
+ class="overlay"
+ style="${styleMap({
+ ...this.overlayStyle,
+ display: this.dragging ? 'block' : 'none',
+ })}"
+ @mousemove="${this.overlayMouseMove}"
+ @mouseleave="${this.releaseFrame}"
+ @mouseup="${this.releaseFrame}"
+ ></div>
+ </div>
+ `;
+ }
+
+ firstUpdated() {
+ this.resizeObserver.observe(this.contentBox);
+ this.resizeObserver.observe(this.contentTransform);
+ }
+
+ updated(changedProperties: PropertyValues) {
+ if (changedProperties.has('frameRect')) {
+ this.updateFrameStyle();
+ }
+ }
+
+ clickOverview(event: MouseEvent) {
+ event.preventDefault();
+
+ this.updateOverlaySize();
+
+ this.dragging = true;
+ const rect = this.content.getBoundingClientRect();
+ this.notifyNewCenter({
+ x: (event.clientX - rect.left) / this.scale,
+ y: (event.clientY - rect.top) / this.scale,
+ });
+ }
+
+ grabFrame(event: MouseEvent) {
+ event.preventDefault();
+ // Do not bubble up into clickOverview().
+ event.stopPropagation();
+
+ this.updateOverlaySize();
+
+ this.dragging = true;
+ const rect = this.frame.getBoundingClientRect();
+ const frameCenterX = rect.x + rect.width / 2;
+ const frameCenterY = rect.y + rect.height / 2;
+ this.grabOffset = {
+ x: event.clientX - frameCenterX,
+ y: event.clientY - frameCenterY,
+ };
+ }
+
+ maybeDragFrame(event: MouseEvent) {
+ event.preventDefault();
+ if (!this.dragging) return;
+ const rect = this.content.getBoundingClientRect();
+ const center = {
+ x: (event.clientX - rect.left - this.grabOffset.x) / this.scale,
+ y: (event.clientY - rect.top - this.grabOffset.y) / this.scale,
+ };
+ this.notifyNewCenter(center);
+ }
+
+ releaseFrame(event: MouseEvent) {
+ event.preventDefault();
+ this.dragging = false;
+ this.grabOffset = {x: 0, y: 0};
+ }
+
+ overlayMouseMove(event: MouseEvent) {
+ event.preventDefault();
+ this.maybeDragFrame(event);
+ }
+
+ private updateScale() {
+ const fitted = fitToFrame(this.imageBounds, this.contentBounds);
+ this.scale = fitted.scale;
+
+ this.contentStyle = {
+ ...this.contentStyle,
+ top: `${fitted.top}px`,
+ left: `${fitted.left}px`,
+ width: `${fitted.width}px`,
+ height: `${fitted.height}px`,
+ };
+
+ this.contentTransformStyle = {
+ transform: `scale(${this.scale})`,
+ };
+
+ this.updateFrameStyle();
+ }
+
+ private updateFrameStyle() {
+ const x = this.frameRect.origin.x * this.scale;
+ const y = this.frameRect.origin.y * this.scale;
+ const width = this.frameRect.dimensions.width * this.scale;
+ const height = this.frameRect.dimensions.height * this.scale;
+ this.frameStyle = {
+ ...this.frameStyle,
+ transform: `translate(${x}px, ${y}px)`,
+ width: `${width}px`,
+ height: `${height}px`,
+ };
+ }
+
+ private updateOverlaySize() {
+ const rect = this.contentBox.getBoundingClientRect();
+ // Create a whole-page overlay to capture mouse events, so that the drag
+ // interaction continues until the user releases the mouse button. Since
+ // innerWidth and innerHeight include scrollbars, we subtract 20 pixels each
+ // to prevent the overlay from extending offscreen under any existing
+ // scrollbar and causing the scrollbar for the other dimension to show up
+ // unnecessarily.
+ const width = window.innerWidth - 20;
+ const height = window.innerHeight - 20;
+ this.overlayStyle = {
+ ...this.overlayStyle,
+ top: `-${rect.top + 1}px`,
+ left: `-${rect.left + 1}px`,
+ width: `${width}px`,
+ height: `${height}px`,
+ };
+ }
+
+ private notifyNewCenter(center: Point) {
+ this.dispatchEvent(
+ new CustomEvent('center-updated', {
+ detail: {...center},
+ bubbles: true,
+ composed: true,
+ })
+ );
+ }
+}
+
+declare global {
+ interface HTMLElementTagNameMap {
+ 'gr-overview-image': GrOverviewImage;
+ }
+}
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-image-viewer/gr-zoomed-image.ts b/polygerrit-ui/app/elements/diff/gr-diff-image-viewer/gr-zoomed-image.ts
new file mode 100644
index 0000000..a14a9cc
--- /dev/null
+++ b/polygerrit-ui/app/elements/diff/gr-diff-image-viewer/gr-zoomed-image.ts
@@ -0,0 +1,95 @@
+/**
+ * @license
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import {
+ css,
+ customElement,
+ html,
+ internalProperty,
+ LitElement,
+ property,
+ PropertyValues,
+} from 'lit-element';
+import {StyleInfo, styleMap} from 'lit-html/directives/style-map';
+import {Rect} from './util';
+
+/**
+ * Displays its slotted content at a given scale, centered over a given point,
+ * while ensuring the content always fills the container. The content does not
+ * have to be a single image, it can be arbitrary HTML. To prevent user
+ * confusion, it should ideally be image-like, i.e. have limited or no
+ * interactivity, as the component does not prevent events or focus from
+ * reaching the slotted content.
+ */
+@customElement('gr-zoomed-image')
+export class GrZoomedImage extends LitElement {
+ @property({type: Number}) scale = 1;
+
+ @property({type: Object})
+ frameRect: Rect = {origin: {x: 0, y: 0}, dimensions: {width: 0, height: 0}};
+
+ @internalProperty() protected imageStyles: StyleInfo = {};
+
+ static styles = css`
+ :host {
+ display: block;
+ }
+ ::slotted(*) {
+ display: block;
+ }
+ #clip {
+ position: relative;
+ width: 100%;
+ height: 100%;
+ overflow: hidden;
+ }
+ #transform {
+ position: absolute;
+ transform-origin: top left;
+ will-change: transform;
+ }
+ `;
+
+ render() {
+ return html`
+ <div id="clip">
+ <div id="transform" style="${styleMap(this.imageStyles)}">
+ <slot></slot>
+ </div>
+ </div>
+ `;
+ }
+
+ updated(changedProperties: PropertyValues) {
+ if (changedProperties.has('scale') || changedProperties.has('frameRect')) {
+ this.updateImageStyles();
+ }
+ }
+
+ private updateImageStyles() {
+ const {x, y} = this.frameRect.origin;
+ this.imageStyles = {
+ 'image-rendering': this.scale >= 1 ? 'pixelated' : 'auto',
+ transform: `translate(${-x}px, ${-y}px) scale(${this.scale})`,
+ };
+ }
+}
+
+declare global {
+ interface HTMLElementTagNameMap {
+ 'gr-zoomed-image': GrZoomedImage;
+ }
+}
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-image-viewer/util.ts b/polygerrit-ui/app/elements/diff/gr-diff-image-viewer/util.ts
new file mode 100644
index 0000000..b42eea9
--- /dev/null
+++ b/polygerrit-ui/app/elements/diff/gr-diff-image-viewer/util.ts
@@ -0,0 +1,236 @@
+/**
+ * @license
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+export interface Point {
+ x: number;
+ y: number;
+}
+
+export interface Dimensions {
+ width: number;
+ height: number;
+}
+
+export interface Rect {
+ origin: Point;
+ dimensions: Dimensions;
+}
+
+export interface FittedContent {
+ top: number;
+ left: number;
+ width: number;
+ height: number;
+ scale: number;
+}
+
+function clamp(value: number, min: number, max: number) {
+ return Math.max(min, Math.min(value, max));
+}
+
+/**
+ * Fits content of the given dimensions into the given frame, maintaining the
+ * aspect ratio of the content and applying letterboxing / pillarboxing as
+ * needed.
+ */
+export function fitToFrame(
+ content: Dimensions,
+ frame: Dimensions
+): FittedContent {
+ const contentAspectRatio = content.width / content.height;
+ const frameAspectRatio = frame.width / frame.height;
+ // If the content is wider than the frame, it will be letterboxed, otherwise
+ // it will be pillarboxed. When letterboxed, content and frame width will
+ // match exactly, when pillarboxed, content and frame height will match
+ // exactly.
+ const isLetterboxed = contentAspectRatio > frameAspectRatio;
+ let width: number;
+ let height: number;
+ if (isLetterboxed) {
+ width = Math.min(frame.width, content.width);
+ height = content.height * (width / content.width);
+ } else {
+ height = Math.min(frame.height, content.height);
+ width = content.width * (height / content.height);
+ }
+ const top = (frame.height - height) / 2;
+ const left = (frame.width - width) / 2;
+ const scale = width / content.width;
+ return {top, left, width, height, scale};
+}
+
+function ensureInBounds(part: Rect, bounds: Dimensions): Rect {
+ const x =
+ part.dimensions.width <= bounds.width
+ ? clamp(part.origin.x, 0, bounds.width - part.dimensions.width)
+ : (bounds.width - part.dimensions.width) / 2;
+ const y =
+ part.dimensions.height <= bounds.height
+ ? clamp(part.origin.y, 0, bounds.height - part.dimensions.height)
+ : (bounds.height - part.dimensions.height) / 2;
+ return {origin: {x, y}, dimensions: part.dimensions};
+}
+
+/**
+ * Maintains a given frame inside given bounds, adjusting requested positions
+ * for the frame as needed. This supports the non-destructive application of a
+ * scaling factor, so that e.g. the magnification of an image can be changed
+ * easily while keeping the frame centered over the same spot. Changing bounds
+ * or frame size also keeps the frame position when possible.
+ */
+export class FrameConstrainer {
+ private center: Point = {x: 0, y: 0};
+
+ private frameSize: Dimensions = {width: 0, height: 0};
+
+ private bounds: Dimensions = {width: 0, height: 0};
+
+ private scale = 1;
+
+ private unscaledFrame: Rect = {
+ origin: {x: 0, y: 0},
+ dimensions: {width: 0, height: 0},
+ };
+
+ private scaledFrame: Rect = {
+ origin: {x: 0, y: 0},
+ dimensions: {width: 0, height: 0},
+ };
+
+ getCenter(): Point {
+ return {...this.center};
+ }
+
+ /**
+ * Returns the frame at its original size, positioned within the given bounds
+ * at the given scale; its origin will be in scaled bounds coordinates.
+ *
+ * Ex: for given bounds 100x50 and frame size 30x20, centered over (50, 25),
+ * all at 1x scale, when setting scale to 2, this will return a frame of size
+ * 30x20, centered over (100, 50), within bounds 200x100.
+ *
+ * Useful for positioning a viewport of fixed size over a magnified image.
+ */
+ getUnscaledFrame(): Rect {
+ return {
+ origin: {...this.unscaledFrame.origin},
+ dimensions: {...this.unscaledFrame.dimensions},
+ };
+ }
+
+ /**
+ * Returns the scaled down frame–a scale of 2 will result in frame dimensions
+ * being halved—position within the given bounds at 1x scale; its origin will
+ * be in unscaled bounds coordinates.
+ *
+ * Ex: for given bounds 100x50 and frame size 30x20, centered over (50, 25),
+ * all at 1x scale, when setting scale to 2, this will return a frame of size
+ * 15x10, centered over (50, 25), within bounds 100x50.
+ *
+ * Useful for highlighting the magnified portion of an image as determined by
+ * getUnscaledFrame() in an overview image of fixed size.
+ */
+ getScaledFrame(): Rect {
+ return {
+ origin: {...this.scaledFrame.origin},
+ dimensions: {...this.scaledFrame.dimensions},
+ };
+ }
+
+ /**
+ * Requests the frame to be centered over the given point, in unscaled bounds
+ * coordinates. This will keep the frame within the given bounds, also when
+ * requesting a center point fully outside the given bounds.
+ */
+ requestCenter(center: Point) {
+ this.center = {...center};
+
+ this.ensureFrameInBounds();
+ }
+
+ /**
+ * Sets the frame size, while keeping the frame within the given bounds, and
+ * maintaining the current center if possible.
+ */
+ setFrameSize(frameSize: Dimensions) {
+ if (frameSize.width <= 0 || frameSize.height <= 0) return;
+ this.frameSize = {...frameSize};
+
+ this.ensureFrameInBounds();
+ }
+
+ /**
+ * Sets the bounds, while keeping the frame within them, and maintaining the
+ * current center if possible.
+ */
+ setBounds(bounds: Dimensions) {
+ if (bounds.width <= 0 || bounds.height <= 0) return;
+ this.bounds = {...bounds};
+
+ this.ensureFrameInBounds();
+ }
+
+ /**
+ * Sets the applied scale, while keeping the frame within the given bounds,
+ * and maintaining the current center if possible (both relevant moving from
+ * a larger scale to a smaller scale).
+ */
+ setScale(scale: number) {
+ if (!scale || scale <= 0) return;
+ this.scale = scale;
+
+ this.ensureFrameInBounds();
+ }
+
+ private ensureFrameInBounds() {
+ const scaledCenter = {
+ x: this.center.x * this.scale,
+ y: this.center.y * this.scale,
+ };
+ const scaledBounds = {
+ width: this.bounds.width * this.scale,
+ height: this.bounds.height * this.scale,
+ };
+ const scaledFrameSize = {
+ width: this.frameSize.width / this.scale,
+ height: this.frameSize.height / this.scale,
+ };
+
+ const requestedUnscaledFrame = {
+ origin: {
+ x: scaledCenter.x - this.frameSize.width / 2,
+ y: scaledCenter.y - this.frameSize.height / 2,
+ },
+ dimensions: this.frameSize,
+ };
+ const requestedScaledFrame = {
+ origin: {
+ x: this.center.x - scaledFrameSize.width / 2,
+ y: this.center.y - scaledFrameSize.height / 2,
+ },
+ dimensions: scaledFrameSize,
+ };
+
+ this.unscaledFrame = ensureInBounds(requestedUnscaledFrame, scaledBounds);
+ this.scaledFrame = ensureInBounds(requestedScaledFrame, this.bounds);
+
+ this.center = {
+ x: this.scaledFrame.origin.x + this.scaledFrame.dimensions.width / 2,
+ y: this.scaledFrame.origin.y + this.scaledFrame.dimensions.height / 2,
+ };
+ }
+}
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-image-viewer/util_test.js b/polygerrit-ui/app/elements/diff/gr-diff-image-viewer/util_test.js
new file mode 100644
index 0000000..80cfa36
--- /dev/null
+++ b/polygerrit-ui/app/elements/diff/gr-diff-image-viewer/util_test.js
@@ -0,0 +1,171 @@
+/**
+ * @license
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import '../../../test/common-test-setup-karma.js';
+import {FrameConstrainer} from './util.js';
+
+suite('FrameConstrainer tests', () => {
+ let constrainer;
+
+ setup(() => {
+ constrainer = new FrameConstrainer();
+ constrainer.setBounds({width: 100, height: 100});
+ constrainer.setFrameSize({width: 50, height: 50});
+ constrainer.requestCenter({x: 50, y: 50});
+ });
+
+ suite('changing center', () => {
+ test('moves frame to requested position', () => {
+ constrainer.requestCenter({x: 30, y: 30});
+ assert.deepEqual(
+ constrainer.getUnscaledFrame(),
+ {origin: {x: 5, y: 5}, dimensions: {width: 50, height: 50}});
+ });
+
+ test('keeps frame in bounds for top left corner', () => {
+ constrainer.requestCenter({x: 5, y: 5});
+ assert.deepEqual(
+ constrainer.getUnscaledFrame(),
+ {origin: {x: 0, y: 0}, dimensions: {width: 50, height: 50}});
+ });
+
+ test('keeps frame in bounds for bottom right corner', () => {
+ constrainer.requestCenter({x: 95, y: 95});
+ assert.deepEqual(
+ constrainer.getUnscaledFrame(),
+ {origin: {x: 50, y: 50}, dimensions: {width: 50, height: 50}});
+ });
+
+ test('handles out-of-bounds center left', () => {
+ constrainer.requestCenter({x: -5, y: 50});
+ assert.deepEqual(
+ constrainer.getUnscaledFrame(),
+ {origin: {x: 0, y: 25}, dimensions: {width: 50, height: 50}});
+ });
+
+ test('handles out-of-bounds center right', () => {
+ constrainer.requestCenter({x: 105, y: 50});
+ assert.deepEqual(
+ constrainer.getUnscaledFrame(),
+ {origin: {x: 50, y: 25}, dimensions: {width: 50, height: 50}});
+ });
+
+ test('handles out-of-bounds center top', () => {
+ constrainer.requestCenter({x: 50, y: -5});
+ assert.deepEqual(
+ constrainer.getUnscaledFrame(),
+ {origin: {x: 25, y: 0}, dimensions: {width: 50, height: 50}});
+ });
+
+ test('handles out-of-bounds center bottom', () => {
+ constrainer.requestCenter({x: 50, y: 105});
+ assert.deepEqual(
+ constrainer.getUnscaledFrame(),
+ {origin: {x: 25, y: 50}, dimensions: {width: 50, height: 50}});
+ });
+ });
+
+ suite('changing frame size', () => {
+ test('maintains center when decreased', () => {
+ constrainer.setFrameSize({width: 10, height: 10});
+ assert.deepEqual(
+ constrainer.getUnscaledFrame(),
+ {origin: {x: 45, y: 45}, dimensions: {width: 10, height: 10}});
+ });
+
+ test('maintains center when increased', () => {
+ constrainer.setFrameSize({width: 80, height: 80});
+ assert.deepEqual(
+ constrainer.getUnscaledFrame(),
+ {origin: {x: 10, y: 10}, dimensions: {width: 80, height: 80}});
+ });
+
+ test('updates center to remain in bounds when increased', () => {
+ constrainer.setFrameSize({width: 10, height: 10});
+ constrainer.requestCenter({x: 95, y: 95});
+ assert.deepEqual(
+ constrainer.getUnscaledFrame(),
+ {origin: {x: 90, y: 90}, dimensions: {width: 10, height: 10}});
+
+ constrainer.setFrameSize({width: 20, height: 20});
+ assert.deepEqual(
+ constrainer.getUnscaledFrame(),
+ {origin: {x: 80, y: 80}, dimensions: {width: 20, height: 20}});
+ });
+ });
+
+ suite('changing scale', () => {
+ suite('for unscaled frame', () => {
+ test('adjusts origin to maintain center when zooming in', () => {
+ constrainer.setScale(2);
+ assert.deepEqual(
+ constrainer.getUnscaledFrame(),
+ {origin: {x: 75, y: 75}, dimensions: {width: 50, height: 50}});
+ });
+
+ test('adjusts origin to maintain center when zooming out', () => {
+ constrainer.setFrameSize({width: 20, height: 20});
+ constrainer.setScale(0.5);
+ assert.deepEqual(
+ constrainer.getUnscaledFrame(),
+ {origin: {x: 15, y: 15}, dimensions: {width: 20, height: 20}});
+ });
+
+ test('keeps frame in bounds when zooming out', () => {
+ constrainer.setScale(5);
+ constrainer.requestCenter({x: 100, y: 100});
+ assert.deepEqual(
+ constrainer.getUnscaledFrame(),
+ {origin: {x: 450, y: 450}, dimensions: {width: 50, height: 50}});
+
+ constrainer.setScale(1);
+ assert.deepEqual(
+ constrainer.getUnscaledFrame(),
+ {origin: {x: 50, y: 50}, dimensions: {width: 50, height: 50}});
+ });
+ });
+
+ suite('for scaled frame', () => {
+ test('decreases frame size and maintains center when zooming in', () => {
+ constrainer.setScale(2);
+ assert.deepEqual(
+ constrainer.getScaledFrame(),
+ {origin: {x: 37.5, y: 37.5}, dimensions: {width: 25, height: 25}});
+ });
+
+ test('increases frame size and maintains center when zooming out', () => {
+ constrainer.setFrameSize({width: 20, height: 20});
+ constrainer.setScale(0.5);
+ assert.deepEqual(
+ constrainer.getScaledFrame(),
+ {origin: {x: 30, y: 30}, dimensions: {width: 40, height: 40}});
+ });
+
+ test('keeps frame in bounds when zooming out', () => {
+ constrainer.setScale(5);
+ constrainer.requestCenter({x: 100, y: 100});
+ assert.deepEqual(
+ constrainer.getScaledFrame(),
+ {origin: {x: 90, y: 90}, dimensions: {width: 10, height: 10}});
+
+ constrainer.setScale(1);
+ assert.deepEqual(
+ constrainer.getScaledFrame(),
+ {origin: {x: 50, y: 50}, dimensions: {width: 50, height: 50}});
+ });
+ });
+ });
+});
\ No newline at end of file
diff --git a/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select_html.ts b/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select_html.ts
index 52465b3..5ab8449 100644
--- a/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select_html.ts
+++ b/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select_html.ts
@@ -35,7 +35,6 @@
text-transform: none;
font-family: var(--font-family);
}
- --trigger-hover-color: rgba(0, 0, 0, 0.6);
}
@media screen and (max-width: 50em) {
.filesWeblinks {
diff --git a/polygerrit-ui/app/elements/settings/gr-cla-view/gr-cla-view_html.ts b/polygerrit-ui/app/elements/settings/gr-cla-view/gr-cla-view_html.ts
index 91ca402..59203d3 100644
--- a/polygerrit-ui/app/elements/settings/gr-cla-view/gr-cla-view_html.ts
+++ b/polygerrit-ui/app/elements/settings/gr-cla-view/gr-cla-view_html.ts
@@ -25,7 +25,7 @@
margin-bottom: var(--spacing-m);
}
.agreementsUrl {
- border: 1px solid #b0bdcc;
+ border: 1px solid var(--border-color);
margin-bottom: var(--spacing-xl);
margin-left: var(--spacing-xl);
margin-right: var(--spacing-xl);
diff --git a/polygerrit-ui/app/elements/shared/gr-account-chip/gr-account-chip_html.ts b/polygerrit-ui/app/elements/shared/gr-account-chip/gr-account-chip_html.ts
index 3bb1458..4e6dd1a 100644
--- a/polygerrit-ui/app/elements/shared/gr-account-chip/gr-account-chip_html.ts
+++ b/polygerrit-ui/app/elements/shared/gr-account-chip/gr-account-chip_html.ts
@@ -58,7 +58,6 @@
gr-button.remove:focus {
--gr-button: {
@apply --gr-remove-button-style;
- color: #333;
}
}
gr-button.remove {
diff --git a/polygerrit-ui/app/elements/shared/gr-avatar/gr-avatar_html.ts b/polygerrit-ui/app/elements/shared/gr-avatar/gr-avatar_html.ts
index d105c5d..e55c8f1 100644
--- a/polygerrit-ui/app/elements/shared/gr-avatar/gr-avatar_html.ts
+++ b/polygerrit-ui/app/elements/shared/gr-avatar/gr-avatar_html.ts
@@ -22,7 +22,7 @@
display: inline-block;
border-radius: 50%;
background-size: cover;
- background-color: var(--avatar-background-color, #f1f2f3);
+ background-color: var(--avatar-background-color, var(--gray-background));
}
</style>
`;
diff --git a/polygerrit-ui/app/elements/shared/gr-comment-thread/gr-comment-thread_html.ts b/polygerrit-ui/app/elements/shared/gr-comment-thread/gr-comment-thread_html.ts
index 76bbd67..55408c0 100644
--- a/polygerrit-ui/app/elements/shared/gr-comment-thread/gr-comment-thread_html.ts
+++ b/polygerrit-ui/app/elements/shared/gr-comment-thread/gr-comment-thread_html.ts
@@ -23,12 +23,12 @@
font-size: var(--font-size-normal);
font-weight: var(--font-weight-normal);
line-height: var(--line-height-normal);
- /* Explicitly set the background color of the diff to be white. We
+ /* Explicitly set the background color of the diff. We
* cannot use the diff content type ab because of the skip chunk preceding
* it, diff processor assumes the chunk of type skip/ab can be collapsed
* and hides our diff behind context control buttons.
* */
- --dark-add-highlight-color: white;
+ --dark-add-highlight-color: var(--background-color-primary);
}
gr-button {
margin-left: var(--spacing-m);
diff --git a/polygerrit-ui/app/elements/shared/gr-linked-chip/gr-linked-chip_html.ts b/polygerrit-ui/app/elements/shared/gr-linked-chip/gr-linked-chip_html.ts
index a335db7..8581a0c 100644
--- a/polygerrit-ui/app/elements/shared/gr-linked-chip/gr-linked-chip_html.ts
+++ b/polygerrit-ui/app/elements/shared/gr-linked-chip/gr-linked-chip_html.ts
@@ -46,7 +46,6 @@
gr-button.remove:focus {
--gr-button: {
@apply --gr-remove-button-style;
- color: #333;
}
}
gr-button.remove {
diff --git a/polygerrit-ui/app/node_modules_licenses/licenses.ts b/polygerrit-ui/app/node_modules_licenses/licenses.ts
index 7a6253b..f03c7e6 100644
--- a/polygerrit-ui/app/node_modules_licenses/licenses.ts
+++ b/polygerrit-ui/app/node_modules_licenses/licenses.ts
@@ -254,6 +254,14 @@
license: SharedLicenses.Polymer2017
},
{
+ name: "@types/resize-observer-browser",
+ license: {
+ name: 'DefinitelyTyped',
+ type: LicenseTypes.Mit,
+ packageLicenseFile: "LICENSE"
+ }
+ },
+ {
name: "@webcomponents/shadycss",
license: SharedLicenses.Polymer2017
},
diff --git a/polygerrit-ui/app/package.json b/polygerrit-ui/app/package.json
index 3351386..5e15990 100644
--- a/polygerrit-ui/app/package.json
+++ b/polygerrit-ui/app/package.json
@@ -25,6 +25,7 @@
"@polymer/paper-tabs": "^3.1.0",
"@polymer/paper-toggle-button": "^3.0.1",
"@polymer/polymer": "^3.4.1",
+ "@types/resize-observer-browser": "^0.1.5",
"@webcomponents/shadycss": "^1.9.2",
"@webcomponents/webcomponentsjs": "^1.3.3",
"ba-linkify": "file:../../lib/ba-linkify/src/",
diff --git a/polygerrit-ui/app/services/checks/checks-util.ts b/polygerrit-ui/app/services/checks/checks-util.ts
index 176464f..1613359 100644
--- a/polygerrit-ui/app/services/checks/checks-util.ts
+++ b/polygerrit-ui/app/services/checks/checks-util.ts
@@ -24,7 +24,7 @@
return undefined;
}
-export function iconForCategory(category: Category) {
+export function iconForCategory(category: Category | 'SUCCESS') {
switch (category) {
case Category.ERROR:
return 'error';
@@ -32,6 +32,8 @@
return 'info-outline';
case Category.WARNING:
return 'warning';
+ case 'SUCCESS':
+ return 'check-circle-outline';
default:
assertNever(category, `Unsupported category: ${category}`);
}
diff --git a/polygerrit-ui/app/styles/gr-voting-styles.ts b/polygerrit-ui/app/styles/gr-voting-styles.ts
index d4e6d52..c1989de 100644
--- a/polygerrit-ui/app/styles/gr-voting-styles.ts
+++ b/polygerrit-ui/app/styles/gr-voting-styles.ts
@@ -27,7 +27,7 @@
<style>
:host {
--vote-chip-styles: {
- border: 1px solid rgba(0,0,0,.12);
+ border: 1px solid var(--border-color);
border-radius: 1em;
box-shadow: none;
box-sizing: border-box;
diff --git a/polygerrit-ui/app/styles/themes/app-theme.ts b/polygerrit-ui/app/styles/themes/app-theme.ts
index c3b0681..2d83cf5 100644
--- a/polygerrit-ui/app/styles/themes/app-theme.ts
+++ b/polygerrit-ui/app/styles/themes/app-theme.ts
@@ -101,6 +101,7 @@
--tooltip-text-color: white;
--negative-red-text-color: #d93025;
--positive-green-text-color: #188038;
+ --indirect-ancestor-text-color: var(--green-700);
/* background colors */
/* primary background colors */
@@ -171,7 +172,7 @@
--line-height-mono: 1.286rem; /* 18px */
--line-height-small: 1.143rem; /* 16px */
--line-height-normal: 1.429rem; /* 20px */
- --line-height-h3: 1.714rem; /* 24px */
+ --line-height-h3: 1.715rem; /* 24px */
--line-height-h2: 2rem; /* 28px */
--line-height-h1: 2.286rem; /* 32px */
--font-weight-normal: 400; /* 400 is the same as 'normal' */
diff --git a/polygerrit-ui/app/styles/themes/dark-theme.ts b/polygerrit-ui/app/styles/themes/dark-theme.ts
index 4057f7f..072ca8f 100644
--- a/polygerrit-ui/app/styles/themes/dark-theme.ts
+++ b/polygerrit-ui/app/styles/themes/dark-theme.ts
@@ -44,9 +44,12 @@
--warning-background: var(--orange-900);
--info-foreground: var(--blue-200);
--info-background: var(--blue-900);
+ --selected-foreground: var(--blue-200);
+ --selected-background: var(--blue-900);
--info-deemphasized-foreground: var(--gray-700);
--info-deemphasized-background: var(--primary-text-color);
--success-foreground: var(--green-200);
+ --success-background: var(--green-900);
--gray-foreground: var(--gray-100);
--gray-background: var(--gray-900);
--tag-background: var(--cyan-900);
diff --git a/polygerrit-ui/app/utils/change-util.ts b/polygerrit-ui/app/utils/change-util.ts
index 1228863..a7f8b49 100644
--- a/polygerrit-ui/app/utils/change-util.ts
+++ b/polygerrit-ui/app/utils/change-util.ts
@@ -173,28 +173,44 @@
return states;
}
-export function isOwner(change?: ChangeInfo, account?: AccountInfo) {
+export function isOwner(change?: ChangeInfo, account?: AccountInfo): boolean {
if (!change || !account) return false;
return change.owner?._account_id === account._account_id;
}
-export function isReviewer(change?: ChangeInfo, account?: AccountInfo) {
+export function isReviewer(
+ change?: ChangeInfo,
+ account?: AccountInfo
+): boolean {
if (!change || !account) return false;
const reviewers = change.reviewers.REVIEWER ?? [];
return reviewers.some(r => r._account_id === account._account_id);
}
-export function isUploader(change?: ChangeInfo, account?: AccountInfo) {
+export function isCc(change?: ChangeInfo, account?: AccountInfo): boolean {
+ if (!change || !account) return false;
+ const ccs = change.reviewers.CC ?? [];
+ return ccs.some(r => r._account_id === account._account_id);
+}
+
+export function isUploader(
+ change?: ChangeInfo,
+ account?: AccountInfo
+): boolean {
if (!change || !account) return false;
const rev = getCurrentRevision(change);
return rev?.uploader?._account_id === account._account_id;
}
-export function isInvolved(change?: ChangeInfo, account?: AccountInfo) {
+export function isInvolved(
+ change?: ChangeInfo,
+ account?: AccountInfo
+): boolean {
const owner = isOwner(change, account);
const uploader = isUploader(change, account);
const reviewer = isReviewer(change, account);
- return owner || uploader || reviewer;
+ const cc = isCc(change, account);
+ return owner || uploader || reviewer || cc;
}
export function getCurrentRevision(change?: ChangeInfo) {
diff --git a/polygerrit-ui/app/utils/label-util.ts b/polygerrit-ui/app/utils/label-util.ts
index 60ac4d8..4eed0a0 100644
--- a/polygerrit-ui/app/utils/label-util.ts
+++ b/polygerrit-ui/app/utils/label-util.ts
@@ -51,3 +51,11 @@
): ApprovalInfo | undefined {
return label.all?.filter(x => x._account_id === account._account_id)[0];
}
+
+export function labelCompare(labelName1: string, labelName2: string) {
+ if (labelName1 === CODE_REVIEW && labelName2 === CODE_REVIEW) return 0;
+ if (labelName1 === CODE_REVIEW) return -1;
+ if (labelName2 === CODE_REVIEW) return 1;
+
+ return labelName1.localeCompare(labelName2);
+}
diff --git a/polygerrit-ui/app/utils/label-util_test.js b/polygerrit-ui/app/utils/label-util_test.js
index 6a2f768..f9a30df 100644
--- a/polygerrit-ui/app/utils/label-util_test.js
+++ b/polygerrit-ui/app/utils/label-util_test.js
@@ -21,6 +21,7 @@
getVotingRangeOrDefault,
getMaxAccounts,
getApprovalInfo,
+ labelCompare,
} from './label-util.js';
const VALUES_1 = {
@@ -113,4 +114,11 @@
};
assert.isUndefined(getApprovalInfo(label, myAccountInfo));
});
+
+ test('labelCompare', () => {
+ let sorted = ['c', 'b', 'a'].sort(labelCompare);
+ assert.sameOrderedMembers(sorted, ['a', 'b', 'c']);
+ sorted = ['b', 'a', 'Code-Review'].sort(labelCompare);
+ assert.sameOrderedMembers(sorted, ['Code-Review', 'a', 'b']);
+ });
});
diff --git a/polygerrit-ui/app/yarn.lock b/polygerrit-ui/app/yarn.lock
index e544dbc..ec3b7a0 100644
--- a/polygerrit-ui/app/yarn.lock
+++ b/polygerrit-ui/app/yarn.lock
@@ -324,6 +324,11 @@
dependencies:
"@webcomponents/shadycss" "^1.9.1"
+"@types/resize-observer-browser@^0.1.5":
+ version "0.1.5"
+ resolved "https://registry.yarnpkg.com/@types/resize-observer-browser/-/resize-observer-browser-0.1.5.tgz#36d897708172ac2380cd486da7a3daf1161c1e23"
+ integrity sha512-8k/67Z95Goa6Lznuykxkfhq9YU3l1Qe6LNZmwde1u7802a3x8v44oq0j91DICclxatTr0rNnhXx7+VTIetSrSQ==
+
"@webcomponents/shadycss@^1.9.1", "@webcomponents/shadycss@^1.9.2":
version "1.9.4"
resolved "https://registry.yarnpkg.com/@webcomponents/shadycss/-/shadycss-1.9.4.tgz#4f9d8ea1526bab084c60b53d4854dc39fdb2bb48"