Merge "Enable error level for the TruthAssertExpected bug pattern in ErrorProne"
diff --git a/Documentation/config-gerrit.txt b/Documentation/config-gerrit.txt
index 76e1f82..84e68ed 100644
--- a/Documentation/config-gerrit.txt
+++ b/Documentation/config-gerrit.txt
@@ -5637,7 +5637,7 @@
Email address that Gerrit refers to itself as when it creates a
new Git commit, such as a merge commit during change submission.
+
-If not set, Gerrit generates this as "gerrit@`hostname`", where
+If not set, Gerrit generates this as "gerrit@``hostname``", where
`hostname` is the hostname of the system Gerrit is running on.
+
By default, not set, generating the value at startup.
diff --git a/Documentation/rest-api-changes.txt b/Documentation/rest-api-changes.txt
index 2b6796b..d83ef0e 100644
--- a/Documentation/rest-api-changes.txt
+++ b/Documentation/rest-api-changes.txt
@@ -600,8 +600,9 @@
----
As a response, two link:#change-info[ChangeInfo] entities are returned
-that describe information added and removed from the `old` change state.
-Only fields that differ between the change's two states are returned.
+that describe information added and removed from the `old` change state, and
+the two link:#change-info[ChangeInfo] entities that generated the diff are
+returned. Only fields that differ between the change's two states are returned.
.Response
----
@@ -625,9 +626,55 @@
"topic": "new-topic"
},
"removed": {
- "updated": "2013-02-20 12:05:34.111000000",
+ "updated": "2013-02-01 09:59:32.126000000",
"topic": "old-topic"
- }
+ },
+ "old_change_info": {
+ "id": "myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940",
+ "project": "myProject",
+ "branch": "master",
+ "attention_set": [],
+ "change_id": "I8473b95934b5732ac55d26311a706c9c2bde9940",
+ "subject": "Implementing Feature X",
+ "status": "NEW",
+ "topic": "old-topic",
+ "created": "2013-02-01 09:59:32.126000000",
+ "updated": "2013-02-01 09:59:32.126000000",
+ "mergeable": true,
+ "insertions": 34,
+ "deletions": 101,
+ "_number": 3965,
+ "owner": {
+ "name": "John Doe"
+ }
+ },
+ "new_change_info": {
+ "id": "myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940",
+ "project": "myProject",
+ "branch": "master",
+ "attention_set": [
+ {
+ "account": {
+ "name": "John Doe"
+ },
+ "last_update": "2013-02-21 11:16:36.775000000",
+ "reason": "reviewer or cc replied"
+ }
+ ],
+ "change_id": "I8473b95934b5732ac55d26311a706c9c2bde9940",
+ "subject": "Implementing Feature X",
+ "status": "NEW",
+ "topic": "new-topic",
+ "created": "2013-02-01 09:59:32.126000000",
+ "updated": "2013-02-21 11:16:36.775000000",
+ "mergeable": true,
+ "insertions": 34,
+ "deletions": 101,
+ "_number": 3965,
+ "owner": {
+ "name": "John Doe"
+ }
+ },
}
----
@@ -3228,6 +3275,8 @@
* are visible to the calling user
* are not already reviewer on the change
* don't own the change
+* are not service users (unless
+ link:config.html#suggest.skipServiceUsers[skipServiceUsers] is set to `false`)
Groups can be excluded from the results by specifying the 'exclude-groups'
request parameter:
@@ -6486,6 +6535,9 @@
Actions the caller might be able to perform on this revision. The
information is a map of view name to link:#action-info[ActionInfo]
entities.
+|`submit_records` ||
+List of the link:rest-api-changes.html#submit-record-info[SubmitRecordInfo]
+containing the submit records for the change at the latest patchset.
|`requirements` |optional|
List of the link:rest-api-changes.html#requirement[requirements] to be met before this change
can be submitted. This field is deprecated in favour of `submit_requirements`.
@@ -8127,6 +8179,37 @@
the failure of the rule predicate.
|===========================
+[[submit-record-info]]
+=== SubmitRecordInfo
+The `SubmitRecordInfo` entity describes results from a submit_rule.
+
+[options="header",cols="1,^1,5"]
+|===========================
+|Field Name ||Description
+|`rule_name`||
+The name of the submit rule that created this submit record. The submit rule is
+specified in the form of "$plugin~$rule" where `$plugin` is the plugin name
+and `$rule` is the name of the class that implemented the submit rule.
+|`status`||
+`OK`, the change can be submitted. +
+`NOT_READY`, additional labels are required before submit. +
+`CLOSED`, closed changes cannot be submitted. +
+`FORCED`, the change was submitted bypassing the submit rule. +
+`RULE_ERROR`, rule code failed with an error.
+|`labels`|optional|
+A list of labels, each containing the following fields. +
+ * `label`: the label name. +
+ * `status`: the label status: {`OK`, `REJECT`, `MAY`, `NEED`, `IMPOSSIBLE`}. +
+ * `appliedBy`: the link:rest-api-accounts.html#account-info[AccountInfo]
+ that applied the vote to the label.
+|`requirements`|optional|
+List of the link:rest-api-changes.html#requirement[requirements] to be met
+before this change can be submitted.
+|`error_message`|optional|
+When status is RULE_ERROR this message provides some text describing
+the failure of the rule predicate.
+|===========================
+
[[submit-requirement-expression-info]]
=== SubmitRequirementExpressionInfo
The `SubmitRequirementExpressionInfo` describes the result of evaluating a
diff --git a/WORKSPACE b/WORKSPACE
index 5c38224..93cae7d 100644
--- a/WORKSPACE
+++ b/WORKSPACE
@@ -922,7 +922,6 @@
yarn_install(
name = "npm",
- data = ["//:twinkie.patch"],
frozen_lockfile = False,
package_json = "//:package.json",
package_path = "",
diff --git a/java/com/google/gerrit/acceptance/ExtensionRegistry.java b/java/com/google/gerrit/acceptance/ExtensionRegistry.java
index 85c4c13..1e5598e 100644
--- a/java/com/google/gerrit/acceptance/ExtensionRegistry.java
+++ b/java/com/google/gerrit/acceptance/ExtensionRegistry.java
@@ -25,6 +25,8 @@
import com.google.gerrit.extensions.events.GitReferenceUpdatedListener;
import com.google.gerrit.extensions.events.GroupIndexedListener;
import com.google.gerrit.extensions.events.ProjectIndexedListener;
+import com.google.gerrit.extensions.events.ReviewerAddedListener;
+import com.google.gerrit.extensions.events.ReviewerDeletedListener;
import com.google.gerrit.extensions.events.RevisionCreatedListener;
import com.google.gerrit.extensions.events.TopicEditedListener;
import com.google.gerrit.extensions.events.WorkInProgressStateChangedListener;
@@ -91,6 +93,8 @@
private final DynamicMap<ProjectConfigEntry> pluginConfigEntries;
private final DynamicSet<PluginPushOption> pluginPushOptions;
private final DynamicSet<OnPostReview> onPostReviews;
+ private final DynamicSet<ReviewerAddedListener> reviewerAddedListeners;
+ private final DynamicSet<ReviewerDeletedListener> reviewerDeletedListeners;
@Inject
ExtensionRegistry(
@@ -125,7 +129,9 @@
DynamicMap<PluginProjectPermissionDefinition> pluginProjectPermissionDefinitions,
DynamicMap<ProjectConfigEntry> pluginConfigEntries,
DynamicSet<PluginPushOption> pluginPushOption,
- DynamicSet<OnPostReview> onPostReviews) {
+ DynamicSet<OnPostReview> onPostReviews,
+ DynamicSet<ReviewerAddedListener> reviewerAddedListeners,
+ DynamicSet<ReviewerDeletedListener> reviewerDeletedListeners) {
this.accountIndexedListeners = accountIndexedListeners;
this.changeIndexedListeners = changeIndexedListeners;
this.groupIndexedListeners = groupIndexedListeners;
@@ -158,6 +164,8 @@
this.pluginConfigEntries = pluginConfigEntries;
this.pluginPushOptions = pluginPushOption;
this.onPostReviews = onPostReviews;
+ this.reviewerAddedListeners = reviewerAddedListeners;
+ this.reviewerDeletedListeners = reviewerDeletedListeners;
}
public Registration newRegistration() {
@@ -302,6 +310,14 @@
return add(onPostReviews, onPostReview);
}
+ public Registration add(ReviewerAddedListener reviewerAddedListener) {
+ return add(reviewerAddedListeners, reviewerAddedListener);
+ }
+
+ public Registration add(ReviewerDeletedListener reviewerDeletedListener) {
+ return add(reviewerDeletedListeners, reviewerDeletedListener);
+ }
+
private <T> Registration add(DynamicSet<T> dynamicSet, T extension) {
return add(dynamicSet, extension, "gerrit");
}
diff --git a/java/com/google/gerrit/entities/SubmitRequirementResult.java b/java/com/google/gerrit/entities/SubmitRequirementResult.java
index 4f2f2f6..b7fa398 100644
--- a/java/com/google/gerrit/entities/SubmitRequirementResult.java
+++ b/java/com/google/gerrit/entities/SubmitRequirementResult.java
@@ -64,6 +64,13 @@
}
}
+ /** Returns true if the submit requirement is fulfilled and can allow change submission. */
+ @Memoized
+ public boolean fulfilled() {
+ Status s = status();
+ return s == Status.SATISFIED || s == Status.OVERRIDDEN || s == Status.NOT_APPLICABLE;
+ }
+
public static Builder builder() {
return new AutoValue_SubmitRequirementResult.Builder();
}
diff --git a/java/com/google/gerrit/extensions/client/ProjectWatchInfo.java b/java/com/google/gerrit/extensions/client/ProjectWatchInfo.java
index 3c20ff7..8f5af76 100644
--- a/java/com/google/gerrit/extensions/client/ProjectWatchInfo.java
+++ b/java/com/google/gerrit/extensions/client/ProjectWatchInfo.java
@@ -54,6 +54,7 @@
}
@Override
+ @SuppressWarnings("OrphanedFormatString")
public String toString() {
StringBuilder b = new StringBuilder();
b.append(project);
diff --git a/java/com/google/gerrit/extensions/common/ChangeInfo.java b/java/com/google/gerrit/extensions/common/ChangeInfo.java
index 6afe8ac..2bb3dd7 100644
--- a/java/com/google/gerrit/extensions/common/ChangeInfo.java
+++ b/java/com/google/gerrit/extensions/common/ChangeInfo.java
@@ -112,6 +112,7 @@
public List<PluginDefinedInfo> plugins;
public Collection<TrackingIdInfo> trackingIds;
public Collection<LegacySubmitRequirementInfo> requirements;
+ public Collection<SubmitRecordInfo> submitRecords;
public Collection<SubmitRequirementResultInfo> submitRequirements;
public ChangeInfo() {}
diff --git a/java/com/google/gerrit/extensions/common/ChangeInfoDiffer.java b/java/com/google/gerrit/extensions/common/ChangeInfoDiffer.java
index ba5e323..ad112d3 100644
--- a/java/com/google/gerrit/extensions/common/ChangeInfoDiffer.java
+++ b/java/com/google/gerrit/extensions/common/ChangeInfoDiffer.java
@@ -63,9 +63,12 @@
*/
public static ChangeInfoDifference getDifference(
ChangeInfo oldChangeInfo, ChangeInfo newChangeInfo) {
- return ChangeInfoDifference.create(
- /* added= */ getAdded(oldChangeInfo, newChangeInfo),
- /* removed= */ getAdded(newChangeInfo, oldChangeInfo));
+ return ChangeInfoDifference.builder()
+ .setOldChangeInfo(oldChangeInfo)
+ .setNewChangeInfo(newChangeInfo)
+ .setAdded(getAdded(oldChangeInfo, newChangeInfo))
+ .setRemoved(getAdded(newChangeInfo, oldChangeInfo))
+ .build();
}
@SuppressWarnings("unchecked") // reflection is used to construct instances of T
diff --git a/java/com/google/gerrit/extensions/common/ChangeInfoDifference.java b/java/com/google/gerrit/extensions/common/ChangeInfoDifference.java
index 269c673..997c3ee 100644
--- a/java/com/google/gerrit/extensions/common/ChangeInfoDifference.java
+++ b/java/com/google/gerrit/extensions/common/ChangeInfoDifference.java
@@ -20,11 +20,29 @@
@AutoValue
public abstract class ChangeInfoDifference {
+ public abstract ChangeInfo oldChangeInfo();
+
+ public abstract ChangeInfo newChangeInfo();
+
public abstract ChangeInfo added();
public abstract ChangeInfo removed();
- public static ChangeInfoDifference create(ChangeInfo added, ChangeInfo removed) {
- return new AutoValue_ChangeInfoDifference(added, removed);
+ public static Builder builder() {
+ return new AutoValue_ChangeInfoDifference.Builder();
+ }
+
+ @AutoValue.Builder
+ public abstract static class Builder {
+
+ public abstract Builder setOldChangeInfo(ChangeInfo oldChangeInfo);
+
+ public abstract Builder setNewChangeInfo(ChangeInfo newChangeInfo);
+
+ public abstract Builder setAdded(ChangeInfo added);
+
+ public abstract Builder setRemoved(ChangeInfo removed);
+
+ public abstract ChangeInfoDifference build();
}
}
diff --git a/java/com/google/gerrit/extensions/common/SubmitRecordInfo.java b/java/com/google/gerrit/extensions/common/SubmitRecordInfo.java
new file mode 100644
index 0000000..09c9841
--- /dev/null
+++ b/java/com/google/gerrit/extensions/common/SubmitRecordInfo.java
@@ -0,0 +1,48 @@
+// 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.extensions.common;
+
+import java.util.List;
+
+/** API response containing a {@link com.google.gerrit.entities.SubmitRecord} entity. */
+public class SubmitRecordInfo {
+ public enum Status {
+ OK,
+ NOT_READY,
+ CLOSED,
+ FORCED,
+ RULE_ERROR
+ }
+
+ public static class Label {
+ public enum Status {
+ OK,
+ REJECT,
+ NEED,
+ MAY,
+ IMPOSSIBLE
+ }
+
+ public String label;
+ public Status status;
+ public AccountInfo appliedBy;
+ }
+
+ public String ruleName;
+ public Status status;
+ public List<Label> labels;
+ public List<LegacySubmitRequirementInfo> requirements;
+ public String errorMessage;
+}
diff --git a/java/com/google/gerrit/server/CommentsUtil.java b/java/com/google/gerrit/server/CommentsUtil.java
index b18f499..ba9f6d6 100644
--- a/java/com/google/gerrit/server/CommentsUtil.java
+++ b/java/com/google/gerrit/server/CommentsUtil.java
@@ -450,8 +450,6 @@
/**
* Get NoteDb draft refs for a change.
*
- * <p>Works if NoteDb is not enabled, but the results are not meaningful.
- *
* <p>This is just a simple ref scan, so the results may potentially include refs for zombie draft
* comments. A zombie draft is one which has been published but the write to delete the draft ref
* from All-Users failed.
diff --git a/java/com/google/gerrit/server/approval/ApprovalInference.java b/java/com/google/gerrit/server/approval/ApprovalInference.java
index 4cb080a..695997a 100644
--- a/java/com/google/gerrit/server/approval/ApprovalInference.java
+++ b/java/com/google/gerrit/server/approval/ApprovalInference.java
@@ -54,6 +54,7 @@
import java.util.Map;
import java.util.Optional;
import org.eclipse.jgit.lib.Config;
+import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.revwalk.RevWalk;
/**
@@ -134,8 +135,9 @@
PatchSet.Id psId,
ChangeKind kind,
LabelType type,
- @Nullable Map<String, FileDiffOutput> modifiedFiles,
- @Nullable Map<String, FileDiffOutput> modifiedFilesLastPatchset) {
+ @Nullable Map<String, FileDiffOutput> baseVsCurrentDiff,
+ @Nullable Map<String, FileDiffOutput> baseVsPriorDiff,
+ @Nullable Map<String, FileDiffOutput> priorVsCurrentDiff) {
int n = psa.key().patchSetId().get();
checkArgument(n != psId.get());
@@ -185,7 +187,8 @@
project.getName());
return true;
} else if (type.isCopyAllScoresIfListOfFilesDidNotChange()
- && listOfFilesUnchangedPredicate.match(modifiedFiles, modifiedFilesLastPatchset)) {
+ && listOfFilesUnchangedPredicate.match(
+ baseVsCurrentDiff, baseVsPriorDiff, priorVsCurrentDiff)) {
logger.atFine().log(
"approval %d on label %s of patch set %d of change %d can be copied"
+ " to patch set %d because the label has set "
@@ -402,8 +405,9 @@
priorPatchSet.getValue().id().changeId(),
changeKind);
- Map<String, FileDiffOutput> modifiedFiles = null;
- Map<String, FileDiffOutput> modifiedFilesLastPatchSet = null;
+ Map<String, FileDiffOutput> baseVsCurrent = null;
+ Map<String, FileDiffOutput> baseVsPrior = null;
+ Map<String, FileDiffOutput> priorVsCurrent = null;
LabelTypes labelTypes = project.getLabelTypes();
for (PatchSetApproval psa : priorApprovals) {
if (resultByUser.contains(psa.label(), psa.accountId())) {
@@ -411,11 +415,13 @@
}
Optional<LabelType> type = labelTypes.byLabel(psa.labelId());
// Only compute modified files if there is a relevant label, since this is expensive.
- if (modifiedFiles == null
+ if (baseVsCurrent == null
&& type.isPresent()
&& type.get().isCopyAllScoresIfListOfFilesDidNotChange()) {
- modifiedFiles = listModifiedFiles(project, patchSet);
- modifiedFilesLastPatchSet = listModifiedFiles(project, priorPatchSet.getValue());
+ baseVsCurrent = listModifiedFiles(project, patchSet);
+ baseVsPrior = listModifiedFiles(project, priorPatchSet.getValue());
+ priorVsCurrent =
+ listModifiedFiles(project, priorPatchSet.getValue().commitId(), patchSet.commitId());
}
if (!type.isPresent()) {
logger.atFine().log(
@@ -435,8 +441,9 @@
patchSet.id(),
changeKind,
type.get(),
- modifiedFiles,
- modifiedFilesLastPatchSet)
+ baseVsCurrent,
+ baseVsPrior,
+ priorVsCurrent)
&& !canCopyBasedOnCopyCondition(notes, psa, patchSet, type.get(), changeKind)) {
continue;
}
@@ -465,4 +472,21 @@
ex);
}
}
+
+ /**
+ * Gets the modified files between two commits corresponding to different patchsets of the same
+ * change.
+ */
+ private Map<String, FileDiffOutput> listModifiedFiles(
+ ProjectState project, ObjectId sourceCommit, ObjectId targetCommit) {
+ try {
+ return diffOperations.listModifiedFiles(project.getNameKey(), sourceCommit, targetCommit);
+ } catch (DiffNotAvailableException ex) {
+ throw new StorageException(
+ "failed to compute difference in files, so won't copy"
+ + " votes on labels even if list of files is the same and "
+ + "copyAllIfListOfFilesDidNotChange",
+ ex);
+ }
+ }
}
diff --git a/java/com/google/gerrit/server/change/AddReviewersOp.java b/java/com/google/gerrit/server/change/AddReviewersOp.java
index a333ce5..cbbd01a 100644
--- a/java/com/google/gerrit/server/change/AddReviewersOp.java
+++ b/java/com/google/gerrit/server/change/AddReviewersOp.java
@@ -219,8 +219,13 @@
.map(r -> accountCache.get(r.accountId()))
.flatMap(Streams::stream)
.collect(toList());
- reviewerAdded.fire(
- ctx.getChangeData(change), patchSet, reviewers, ctx.getAccount(), ctx.getWhen());
+ eventSender =
+ () ->
+ reviewerAdded.fire(
+ ctx.getChangeData(change), patchSet, reviewers, ctx.getAccount(), ctx.getWhen());
+ if (sendEvent) {
+ sendEvent();
+ }
}
}
}
diff --git a/java/com/google/gerrit/server/change/ChangeJson.java b/java/com/google/gerrit/server/change/ChangeJson.java
index 5efcf59..db25dc7 100644
--- a/java/com/google/gerrit/server/change/ChangeJson.java
+++ b/java/com/google/gerrit/server/change/ChangeJson.java
@@ -79,6 +79,7 @@
import com.google.gerrit.extensions.common.ProblemInfo;
import com.google.gerrit.extensions.common.ReviewerUpdateInfo;
import com.google.gerrit.extensions.common.RevisionInfo;
+import com.google.gerrit.extensions.common.SubmitRecordInfo;
import com.google.gerrit.extensions.common.SubmitRequirementExpressionInfo;
import com.google.gerrit.extensions.common.SubmitRequirementResultInfo;
import com.google.gerrit.extensions.common.TrackingIdInfo;
@@ -369,6 +370,14 @@
return reqInfos;
}
+ private Collection<SubmitRecordInfo> submitRecordsFor(ChangeData cd) {
+ List<SubmitRecordInfo> submitRecordInfos = new ArrayList<>();
+ for (SubmitRecord record : cd.submitRecords(SUBMIT_RULE_OPTIONS_STRICT)) {
+ submitRecordInfos.add(submitRecordToInfo(record));
+ }
+ return submitRecordInfos;
+ }
+
private static Collection<SubmitRequirementResultInfo> submitRequirementsFor(ChangeData cd) {
Collection<SubmitRequirementResultInfo> reqInfos = new ArrayList<>();
Map<SubmitRequirement, SubmitRequirementResult> requirements = cd.submitRequirements();
@@ -383,6 +392,34 @@
return new LegacySubmitRequirementInfo(status.name(), req.fallbackText(), req.type());
}
+ private SubmitRecordInfo submitRecordToInfo(SubmitRecord record) {
+ SubmitRecordInfo info = new SubmitRecordInfo();
+ if (record.status != null) {
+ info.status = SubmitRecordInfo.Status.valueOf(record.status.name());
+ }
+ info.ruleName = record.ruleName;
+ info.errorMessage = record.errorMessage;
+ if (record.labels != null) {
+ info.labels = new ArrayList<>();
+ for (SubmitRecord.Label label : record.labels) {
+ SubmitRecordInfo.Label labelInfo = new SubmitRecordInfo.Label();
+ labelInfo.label = label.label;
+ if (label.status != null) {
+ labelInfo.status = SubmitRecordInfo.Label.Status.valueOf(label.status.name());
+ }
+ labelInfo.appliedBy = accountLoader.get(label.appliedBy);
+ info.labels.add(labelInfo);
+ }
+ }
+ if (record.requirements != null) {
+ info.requirements = new ArrayList<>();
+ for (LegacySubmitRequirement requirement : record.requirements) {
+ info.requirements.add(requirementToInfo(requirement, record.status));
+ }
+ }
+ return info;
+ }
+
private static SubmitRequirementResultInfo submitRequirementToInfo(
SubmitRequirement req, SubmitRequirementResult result) {
SubmitRequirementResultInfo info = new SubmitRequirementResultInfo();
@@ -662,6 +699,7 @@
out.labels = labelsJson.labelsFor(accountLoader, cd, has(LABELS), has(DETAILED_LABELS));
out.requirements = requirementsFor(cd);
+ out.submitRecords = submitRecordsFor(cd);
if (has(SUBMIT_REQUIREMENTS)) {
out.submitRequirements = submitRequirementsFor(cd);
}
diff --git a/java/com/google/gerrit/server/change/DeleteReviewerOp.java b/java/com/google/gerrit/server/change/DeleteReviewerOp.java
index a26f107..1e40429 100644
--- a/java/com/google/gerrit/server/change/DeleteReviewerOp.java
+++ b/java/com/google/gerrit/server/change/DeleteReviewerOp.java
@@ -202,16 +202,23 @@
"Cannot email update for change %s", currChange.getId());
}
}
- reviewerDeleted.fire(
- ctx.getChangeData(currChange),
- patchSet,
- accountCache.get(reviewer.id()).orElse(AccountState.forAccount(reviewer)),
- ctx.getAccount(),
- mailMessage,
- newApprovals,
- oldApprovals,
- notify.handling(),
- ctx.getWhen());
+
+ NotifyHandling notifyHandling = notify.handling();
+ eventSender =
+ () ->
+ reviewerDeleted.fire(
+ ctx.getChangeData(currChange),
+ patchSet,
+ accountCache.get(reviewer.id()).orElse(AccountState.forAccount(reviewer)),
+ ctx.getAccount(),
+ mailMessage,
+ newApprovals,
+ oldApprovals,
+ notifyHandling,
+ ctx.getWhen());
+ if (sendEvent) {
+ sendEvent();
+ }
}
private Iterable<PatchSetApproval> approvals(ChangeContext ctx, Account.Id accountId) {
diff --git a/java/com/google/gerrit/server/change/GetRelatedChangesUtil.java b/java/com/google/gerrit/server/change/GetRelatedChangesUtil.java
new file mode 100644
index 0000000..b1f9726
--- /dev/null
+++ b/java/com/google/gerrit/server/change/GetRelatedChangesUtil.java
@@ -0,0 +1,119 @@
+// 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.change;
+
+import static com.google.common.base.Preconditions.checkArgument;
+import static java.util.stream.Collectors.toSet;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.entities.PatchSet;
+import com.google.gerrit.index.IndexConfig;
+import com.google.gerrit.server.permissions.PermissionBackendException;
+import com.google.gerrit.server.query.change.ChangeData;
+import com.google.gerrit.server.query.change.InternalChangeQuery;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import com.google.inject.Singleton;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.Set;
+
+/** Utility class that gets the ancestor changes and the descendent changes of a specific change. */
+@Singleton
+public class GetRelatedChangesUtil {
+ private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+
+ private final Provider<InternalChangeQuery> queryProvider;
+ private final RelatedChangesSorter sorter;
+ private final IndexConfig indexConfig;
+ private final ChangeData.Factory changeDataFactory;
+
+ @Inject
+ GetRelatedChangesUtil(
+ Provider<InternalChangeQuery> queryProvider,
+ RelatedChangesSorter sorter,
+ IndexConfig indexConfig,
+ ChangeData.Factory changeDataFactory) {
+ this.queryProvider = queryProvider;
+ this.sorter = sorter;
+ this.indexConfig = indexConfig;
+ this.changeDataFactory = changeDataFactory;
+ }
+
+ /**
+ * Gets related changes of a specific change revision.
+ *
+ * @param changeData the change of the inputted revision.
+ * @param basePs the revision that the method checks for related changes.
+ * @return list of related changes, sorted via {@link RelatedChangesSorter}
+ */
+ public List<RelatedChangesSorter.PatchSetData> getRelated(ChangeData changeData, PatchSet basePs)
+ throws IOException, PermissionBackendException {
+ Set<String> groups = getAllGroups(changeData.patchSets());
+ logger.atFine().log("groups = %s", groups);
+ if (groups.isEmpty()) {
+ return Collections.emptyList();
+ }
+
+ List<ChangeData> cds =
+ InternalChangeQuery.byProjectGroups(
+ queryProvider, indexConfig, changeData.project(), groups);
+ if (cds.isEmpty()) {
+ return Collections.emptyList();
+ }
+ if (cds.size() == 1 && cds.get(0).getId().equals(changeData.getId())) {
+ return Collections.emptyList();
+ }
+
+ cds = reloadChangeIfStale(cds, changeData, basePs);
+
+ return sorter.sort(cds, basePs);
+ }
+
+ private List<ChangeData> reloadChangeIfStale(
+ List<ChangeData> changeDatasFromIndex, ChangeData wantedChange, PatchSet wantedPs) {
+ checkArgument(
+ wantedChange.getId().equals(wantedPs.id().changeId()),
+ "change of wantedPs (%s) doesn't match wantedChange (%s)",
+ wantedPs.id().changeId(),
+ wantedChange.getId());
+
+ List<ChangeData> changeDatas = new ArrayList<>(changeDatasFromIndex.size() + 1);
+ changeDatas.addAll(changeDatasFromIndex);
+
+ // Reload the change in case the patch set is absent.
+ changeDatas.stream()
+ .filter(
+ cd -> cd.getId().equals(wantedPs.id().changeId()) && cd.patchSet(wantedPs.id()) == null)
+ .forEach(ChangeData::reloadChange);
+
+ if (changeDatas.stream().noneMatch(cd -> cd.getId().equals(wantedPs.id().changeId()))) {
+ // The change of the wanted patch set is missing in the result from the index.
+ // Load it from NoteDb and add it to the result.
+ changeDatas.add(changeDataFactory.create(wantedChange.change()));
+ }
+
+ return changeDatas;
+ }
+
+ @VisibleForTesting
+ public static Set<String> getAllGroups(Collection<PatchSet> patchSets) {
+ return patchSets.stream().flatMap(ps -> ps.groups().stream()).collect(toSet());
+ }
+}
diff --git a/java/com/google/gerrit/server/restapi/change/RelatedChangesSorter.java b/java/com/google/gerrit/server/change/RelatedChangesSorter.java
similarity index 96%
rename from java/com/google/gerrit/server/restapi/change/RelatedChangesSorter.java
rename to java/com/google/gerrit/server/change/RelatedChangesSorter.java
index 0634081..547452e 100644
--- a/java/com/google/gerrit/server/restapi/change/RelatedChangesSorter.java
+++ b/java/com/google/gerrit/server/change/RelatedChangesSorter.java
@@ -11,8 +11,7 @@
// 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.restapi.change;
+package com.google.gerrit.server.change;
import static com.google.common.base.Preconditions.checkArgument;
import static java.util.Objects.requireNonNull;
@@ -30,6 +29,7 @@
import com.google.gerrit.entities.PatchSet;
import com.google.gerrit.entities.Project;
import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.server.change.RelatedChangesSorter.PatchSetData;
import com.google.gerrit.server.git.GitRepositoryManager;
import com.google.gerrit.server.permissions.ChangePermission;
import com.google.gerrit.server.permissions.PermissionBackend;
@@ -57,7 +57,7 @@
import org.eclipse.jgit.revwalk.RevWalk;
@Singleton
-class RelatedChangesSorter {
+public class RelatedChangesSorter {
private final GitRepositoryManager repoManager;
private final PermissionBackend permissionBackend;
private final ProjectCache projectCache;
@@ -247,23 +247,23 @@
}
@AutoValue
- abstract static class PatchSetData {
+ public abstract static class PatchSetData {
@VisibleForTesting
static PatchSetData create(ChangeData cd, PatchSet ps, RevCommit commit) {
return new AutoValue_RelatedChangesSorter_PatchSetData(cd, ps, commit);
}
- abstract ChangeData data();
+ public abstract ChangeData data();
- abstract PatchSet patchSet();
+ public abstract PatchSet patchSet();
- abstract RevCommit commit();
+ public abstract RevCommit commit();
- PatchSet.Id psId() {
+ public PatchSet.Id psId() {
return patchSet().id();
}
- Change.Id id() {
+ public Change.Id id() {
return psId().changeId();
}
diff --git a/java/com/google/gerrit/server/change/ReviewerOp.java b/java/com/google/gerrit/server/change/ReviewerOp.java
index 716ac5e..12227c2 100644
--- a/java/com/google/gerrit/server/change/ReviewerOp.java
+++ b/java/com/google/gerrit/server/change/ReviewerOp.java
@@ -32,6 +32,8 @@
public class ReviewerOp implements BatchUpdateOp {
protected boolean sendEmail = true;
+ protected boolean sendEvent = true;
+ protected Runnable eventSender = () -> {};
protected PatchSet patchSet;
protected Result opResult;
@@ -42,6 +44,14 @@
this.sendEmail = false;
}
+ public void suppressEvent() {
+ this.sendEvent = false;
+ }
+
+ public void sendEvent() {
+ eventSender.run();
+ }
+
void setPatchSet(PatchSet patchSet) {
this.patchSet = requireNonNull(patchSet);
}
diff --git a/java/com/google/gerrit/server/notedb/CommitRewriter.java b/java/com/google/gerrit/server/notedb/CommitRewriter.java
index 7d743dc..e940b1e 100644
--- a/java/com/google/gerrit/server/notedb/CommitRewriter.java
+++ b/java/com/google/gerrit/server/notedb/CommitRewriter.java
@@ -20,6 +20,7 @@
import static com.google.gerrit.server.notedb.ChangeNoteUtil.FOOTER_LABEL;
import static com.google.gerrit.server.notedb.ChangeNoteUtil.FOOTER_REAL_USER;
import static com.google.gerrit.server.notedb.ChangeNoteUtil.FOOTER_SUBMITTED_WITH;
+import static com.google.gerrit.server.notedb.ChangeNoteUtil.FOOTER_TAG;
import static com.google.gerrit.server.util.AccountTemplateUtil.ACCOUNT_TEMPLATE_PATTERN;
import static com.google.gerrit.server.util.AccountTemplateUtil.ACCOUNT_TEMPLATE_REGEX;
@@ -39,6 +40,7 @@
import com.google.gerrit.entities.SubmitRecord;
import com.google.gerrit.git.RefUpdateUtil;
import com.google.gerrit.json.OutputFormat;
+import com.google.gerrit.server.ChangeMessagesUtil;
import com.google.gerrit.server.account.AccountCache;
import com.google.gerrit.server.account.AccountState;
import com.google.gerrit.server.account.externalids.ExternalId;
@@ -58,7 +60,6 @@
import java.util.HashSet;
import java.util.List;
import java.util.Map;
-import java.util.Map.Entry;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
@@ -170,7 +171,8 @@
Pattern.compile("Assignee changed from: (.*) to: (.*)");
private static final Pattern REMOVED_REVIEWER_PATTERN =
- Pattern.compile("Removed (cc|reviewer) (.*)(\\.| with the following votes)");
+ Pattern.compile(
+ "Removed (cc|reviewer) (.*)(\\.| with the following votes:\n.*)", Pattern.DOTALL);
private static final Pattern REMOVED_VOTE_PATTERN = Pattern.compile("Removed (.*) by (.*)");
@@ -186,12 +188,18 @@
private static final Pattern ON_CODE_OWNER_ADD_REVIEWER_PATTERN =
Pattern.compile("(.*) who was added as reviewer owns the following files");
+
+ private static final String CODE_OWNER_ADD_REVIEWER_TAG =
+ ChangeMessagesUtil.AUTOGENERATED_BY_GERRIT_TAG_PREFIX + "code-owners:addReviewer";
+
private static final String ON_CODE_OWNER_APPROVAL_REGEX = "code-owner approved by (.*):";
private static final String ON_CODE_OWNER_OVERRIDE_REGEX =
"code-owners submit requirement .* overridden by (.*)";
private static final Pattern ON_CODE_OWNER_REVIEW_PATTERN =
Pattern.compile(ON_CODE_OWNER_APPROVAL_REGEX + "|" + ON_CODE_OWNER_OVERRIDE_REGEX);
+ private static final Pattern ON_CODE_OWNER_POST_REVIEW_PATTERN =
+ Pattern.compile("Patch Set [0-9]+:[\\s\\S]*By (voting|removing)[\\s\\S]*");
private static final Pattern REPLY_BY_REASON_PATTERN =
Pattern.compile("(.*) replied on the change");
@@ -386,7 +394,7 @@
RevCommit originalCommit;
boolean rewriteStarted = false;
- ChangeFixProgress changeFixProgress = new ChangeFixProgress();
+ ChangeFixProgress changeFixProgress = new ChangeFixProgress(ref.getName());
while ((originalCommit = revWalk.next()) != null) {
changeFixProgress.updateAuthorId =
@@ -535,10 +543,16 @@
Matcher assigneeDeletedMatcher = ASSIGNEE_DELETED_PATTERN.matcher(originalChangeMessage);
if (assigneeDeletedMatcher.matches()) {
if (!NON_REPLACE_ACCOUNT_PATTERN.matcher(assigneeDeletedMatcher.group(1)).matches()) {
+ Optional<String> assigneeReplacement =
+ getPossibleAccountReplacement(
+ changeFixProgress,
+ oldAssignee,
+ getAccountInfoFromNameEmail(assigneeDeletedMatcher.group(1)));
+
return Optional.of(
- "Assignee deleted: "
- + getPossibleAccountReplacement(
- changeFixProgress, oldAssignee, assigneeDeletedMatcher.group(1)));
+ assigneeReplacement.isPresent()
+ ? "Assignee deleted: " + assigneeReplacement.get()
+ : "Assignee was deleted.");
}
return Optional.empty();
}
@@ -546,10 +560,15 @@
Matcher assigneeAddedMatcher = ASSIGNEE_ADDED_PATTERN.matcher(originalChangeMessage);
if (assigneeAddedMatcher.matches()) {
if (!NON_REPLACE_ACCOUNT_PATTERN.matcher(assigneeAddedMatcher.group(1)).matches()) {
+ Optional<String> assigneeReplacement =
+ getPossibleAccountReplacement(
+ changeFixProgress,
+ newAssignee,
+ getAccountInfoFromNameEmail(assigneeAddedMatcher.group(1)));
return Optional.of(
- "Assignee added: "
- + getPossibleAccountReplacement(
- changeFixProgress, newAssignee, assigneeAddedMatcher.group(1)));
+ assigneeReplacement.isPresent()
+ ? "Assignee added: " + assigneeReplacement.get()
+ : "Assignee was added.");
}
return Optional.empty();
}
@@ -557,13 +576,22 @@
Matcher assigneeChangedMatcher = ASSIGNEE_CHANGED_PATTERN.matcher(originalChangeMessage);
if (assigneeChangedMatcher.matches()) {
if (!NON_REPLACE_ACCOUNT_PATTERN.matcher(assigneeChangedMatcher.group(1)).matches()) {
+ Optional<String> oldAssigneeReplacement =
+ getPossibleAccountReplacement(
+ changeFixProgress,
+ oldAssignee,
+ getAccountInfoFromNameEmail(assigneeChangedMatcher.group(1)));
+ Optional<String> newAssigneeReplacement =
+ getPossibleAccountReplacement(
+ changeFixProgress,
+ newAssignee,
+ getAccountInfoFromNameEmail(assigneeChangedMatcher.group(2)));
return Optional.of(
- String.format(
- "Assignee changed from: %s to: %s",
- getPossibleAccountReplacement(
- changeFixProgress, oldAssignee, assigneeChangedMatcher.group(1)),
- getPossibleAccountReplacement(
- changeFixProgress, newAssignee, assigneeChangedMatcher.group(2))));
+ oldAssigneeReplacement.isPresent() && newAssigneeReplacement.isPresent()
+ ? String.format(
+ "Assignee changed from: %s to: %s",
+ oldAssigneeReplacement.get(), newAssigneeReplacement.get())
+ : "Assignee was changed.");
}
return Optional.empty();
}
@@ -576,7 +604,7 @@
}
Matcher matcher = REMOVED_REVIEWER_PATTERN.matcher(originalChangeMessage);
- if (matcher.find() && !ACCOUNT_TEMPLATE_PATTERN.matcher(matcher.group(2)).matches()) {
+ if (matcher.matches() && !ACCOUNT_TEMPLATE_PATTERN.matcher(matcher.group(2)).matches()) {
// Since we do not use change messages for reviewer updates on UI, it does not matter what we
// rewrite it to.
return Optional.of(originalChangeMessage.substring(0, matcher.end(1)));
@@ -594,12 +622,15 @@
Matcher matcher = REMOVED_VOTE_PATTERN.matcher(originalChangeMessage);
if (matcher.matches() && !NON_REPLACE_ACCOUNT_PATTERN.matcher(matcher.group(2)).matches()) {
- return Optional.of(
- String.format(
- "Removed %s by %s",
- matcher.group(1),
- getPossibleAccountReplacement(
- changeFixProgress, reviewer, getNameFromNameEmail(matcher.group(2)))));
+ Optional<String> reviewerReplacement =
+ getPossibleAccountReplacement(
+ changeFixProgress, reviewer, getAccountInfoFromNameEmail(matcher.group(2)));
+ StringBuilder replacement = new StringBuilder();
+ replacement.append("Removed ").append(matcher.group(1));
+ if (reviewerReplacement.isPresent()) {
+ replacement.append(" by ").append(reviewerReplacement.get());
+ }
+ return Optional.of(replacement.toString());
}
return Optional.empty();
}
@@ -612,21 +643,27 @@
}
String[] lines = originalChangeMessage.split("\\r?\\n");
StringBuilder fixedLines = new StringBuilder();
+ boolean anyFixed = false;
for (int i = 1; i < lines.length; i++) {
if (lines[i].isEmpty()) {
continue;
}
Matcher matcher = REMOVED_VOTES_CHANGE_MESSAGE_PATTERN.matcher(lines[i]);
+ String replacementLine = lines[i];
if (matcher.matches() && !NON_REPLACE_ACCOUNT_PATTERN.matcher(matcher.group(2)).matches()) {
- fixedLines.append(
- String.format(
- "* %s by %s\n",
- matcher.group(1),
- getPossibleAccountReplacement(
- changeFixProgress, Optional.empty(), getNameFromNameEmail(matcher.group(2)))));
+ anyFixed = true;
+ Optional<String> reviewerReplacement =
+ getPossibleAccountReplacement(
+ changeFixProgress, Optional.empty(), getAccountInfoFromNameEmail(matcher.group(2)));
+ replacementLine = "* " + matcher.group(1);
+ if (reviewerReplacement.isPresent()) {
+ replacementLine += " by " + reviewerReplacement.get();
+ }
+ replacementLine += "\n";
}
+ fixedLines.append(replacementLine);
}
- if (fixedLines.length() == 0) {
+ if (!anyFixed) {
return Optional.empty();
}
return Optional.of(REMOVED_VOTES_CHANGE_MESSAGE_START + "\n" + fixedLines);
@@ -686,10 +723,14 @@
StringBuffer sb = new StringBuffer();
while (onAddReviewerMatcher.find()) {
String reviewerName = normalizeOnCodeOwnerAddReviewerMatch(onAddReviewerMatcher.group(1));
- String replacementName =
- getPossibleAccountReplacement(changeFixProgress, Optional.empty(), reviewerName);
+ Optional<String> replacementName =
+ getPossibleAccountReplacement(
+ changeFixProgress, Optional.empty(), ParsedAccountInfo.create(reviewerName));
onAddReviewerMatcher.appendReplacement(
- sb, replacementName + ", who was added as reviewer owns the following files");
+ sb,
+ replacementName.isPresent()
+ ? replacementName.get() + ", who was added as reviewer owns the following files"
+ : "Added reviewer owns the following files");
}
onAddReviewerMatcher.appendTail(sb);
sb.append("\n");
@@ -717,7 +758,11 @@
if (Strings.isNullOrEmpty(originalMessage)) {
return Optional.empty();
}
-
+ Matcher onCodeOwnerPostReviewMatcher =
+ ON_CODE_OWNER_POST_REVIEW_PATTERN.matcher(originalMessage);
+ if (!onCodeOwnerPostReviewMatcher.matches()) {
+ return Optional.empty();
+ }
Matcher onCodeOwnerReviewMatcher = ON_CODE_OWNER_REVIEW_PATTERN.matcher(originalMessage);
while (onCodeOwnerReviewMatcher.find()) {
String accountName =
@@ -809,7 +854,9 @@
for (FooterLine fl : footerLines) {
String footerKey = fl.getKey();
String footerValue = fl.getValue();
- if (footerKey.equalsIgnoreCase(FOOTER_ASSIGNEE.getName())) {
+ if (footerKey.equalsIgnoreCase(FOOTER_TAG.getName())) {
+ fixProgress.tag = footerValue;
+ } else if (footerKey.equalsIgnoreCase(FOOTER_ASSIGNEE.getName())) {
Account.Id oldAssignee = fixProgress.assigneeId;
FixIdentResult fixedAssignee = null;
if (footerValue.equals("")) {
@@ -940,7 +987,8 @@
fixedChangeMessage =
fixCodeOwnersOnReviewChangeMessage(fixProgress.updateAuthorId, originalChangeMessage);
}
- if (!fixedChangeMessage.isPresent()) {
+ if (!fixedChangeMessage.isPresent()
+ && Objects.equals(fixProgress.tag, CODE_OWNER_ADD_REVIEWER_TAG)) {
fixedChangeMessage =
fixCodeOwnersOnAddReviewerChangeMessage(fixProgress, originalChangeMessage);
}
@@ -971,9 +1019,10 @@
private Optional<Account.Id> parseIdent(ChangeFixProgress changeFixProgress, PersonIdent ident) {
Optional<Account.Id> account = NoteDbUtil.parseIdent(ident);
if (account.isPresent()) {
- changeFixProgress.parsedAccounts.putIfAbsent(account.get(), "");
+ changeFixProgress.parsedAccounts.putIfAbsent(account.get(), Optional.empty());
} else {
- logger.atWarning().log("Failed to parse id %s", ident);
+ logger.atWarning().log(
+ "Fixing ref %s, failed to parse id %s", changeFixProgress.changeMetaRef, ident);
}
return account;
}
@@ -1022,10 +1071,16 @@
return fixIdentResult;
}
- /** Extracts {@link Account#getName} from {@link Account#getNameEmail} */
- private String getNameFromNameEmail(String nameEmail) {
+ /** Extracts {@link ParsedAccountInfo} from {@link Account#getNameEmail} */
+ private ParsedAccountInfo getAccountInfoFromNameEmail(String nameEmail) {
Matcher nameEmailMatcher = NAME_EMAIL_PATTERN.matcher(nameEmail);
- return nameEmailMatcher.matches() ? nameEmailMatcher.group(1) : nameEmail;
+ if (!nameEmailMatcher.matches()) {
+ return ParsedAccountInfo.create(nameEmail);
+ }
+
+ return ParsedAccountInfo.create(
+ nameEmailMatcher.group(1),
+ nameEmailMatcher.group(2).substring(1, nameEmailMatcher.group(2).length() - 1));
}
/**
@@ -1034,43 +1089,79 @@
* <p>If {@code account} is known, replace with {@link AccountTemplateUtil#getAccountTemplate}.
* Otherwise, try to guess the correct replacement account for {@code accountName} among {@link
* ChangeFixProgress#parsedAccounts} that appeared in the change. If this fails {@link
- * #DEFAULT_ACCOUNT_REPLACEMENT} is applied.
+ * Optional#empty} is returned.
*
* @param changeFixProgress see {@link ChangeFixProgress}
* @param account account that should be used for replacement, if known
- * @param accountName {@link Account#getName} to replace.
- * @return replacement for {@code accountName}
+ * @param accountInfo {@link ParsedAccountInfo} to replace.
+ * @return replacement for {@code accountName} or {@link Optional#empty}, if the replacement could
+ * not be determined.
*/
- private String getPossibleAccountReplacement(
- ChangeFixProgress changeFixProgress, Optional<Account.Id> account, String accountName) {
+ private Optional<String> getPossibleAccountReplacement(
+ ChangeFixProgress changeFixProgress,
+ Optional<Account.Id> account,
+ ParsedAccountInfo accountInfo) {
if (account.isPresent()) {
- return AccountTemplateUtil.getAccountTemplate(account.get());
+ return Optional.of(AccountTemplateUtil.getAccountTemplate(account.get()));
}
// Retrieve reviewer accounts from cache and try to match by their name.
- Map<Account.Id, AccountState> missingUserNameReviewers =
+ Map<Account.Id, AccountState> missingAccountStateReviewers =
accountCache.get(
changeFixProgress.parsedAccounts.entrySet().stream()
- .filter(entry -> entry.getValue().isEmpty())
+ .filter(entry -> !entry.getValue().isPresent())
.map(Map.Entry::getKey)
.collect(ImmutableSet.toImmutableSet()));
changeFixProgress.parsedAccounts.putAll(
- missingUserNameReviewers.entrySet().stream()
+ missingAccountStateReviewers.entrySet().stream()
.collect(
ImmutableMap.toImmutableMap(
- Map.Entry::getKey, e -> e.getValue().account().getName())));
- Set<Account.Id> possibleReplacements =
- changeFixProgress.parsedAccounts.entrySet().stream()
- .filter(e -> e.getValue().equals(accountName))
- .map(Entry::getKey)
- .collect(ImmutableSet.toImmutableSet());
- String replacementName = DEFAULT_ACCOUNT_REPLACEMENT;
+ Map.Entry::getKey, e -> Optional.ofNullable(e.getValue()))));
+ Map<Account.Id, AccountState> possibleReplacements = ImmutableMap.of();
+ if (accountInfo.email().isPresent()) {
+ possibleReplacements =
+ changeFixProgress.parsedAccounts.entrySet().stream()
+ .filter(
+ e ->
+ e.getValue().isPresent()
+ && Objects.equals(
+ e.getValue().get().account().preferredEmail(),
+ accountInfo.email().get()))
+ .collect(ImmutableMap.toImmutableMap(Map.Entry::getKey, e -> e.getValue().get()));
+ // Filter further so we match both email & name
+ if (possibleReplacements.size() > 1) {
+ logger.atWarning().log(
+ "Fixing ref %s, multiple accounts found with the same email address, while replacing %s",
+ changeFixProgress.changeMetaRef, accountInfo);
+ possibleReplacements =
+ possibleReplacements.entrySet().stream()
+ .filter(e -> Objects.equals(e.getValue().account().getName(), accountInfo.name()))
+ .collect(ImmutableMap.toImmutableMap(Map.Entry::getKey, Map.Entry::getValue));
+ }
+ }
if (possibleReplacements.isEmpty()) {
- logger.atWarning().log("Could not find reviewer account matching name %s", accountName);
+ possibleReplacements =
+ changeFixProgress.parsedAccounts.entrySet().stream()
+ .filter(
+ e ->
+ e.getValue().isPresent()
+ && Objects.equals(
+ e.getValue().get().account().getName(), accountInfo.name()))
+ .collect(ImmutableMap.toImmutableMap(Map.Entry::getKey, e -> e.getValue().get()));
+ }
+ Optional<String> replacementName = Optional.empty();
+ if (possibleReplacements.isEmpty()) {
+ logger.atWarning().log(
+ "Fixing ref %s, could not find reviewer account matching name %s",
+ changeFixProgress.changeMetaRef, accountInfo);
} else if (possibleReplacements.size() > 1) {
- logger.atWarning().log("Found multiple reviewer account matching name %s", accountName);
+ logger.atWarning().log(
+ "Fixing ref %s found multiple reviewer account matching name %s",
+ changeFixProgress.changeMetaRef, accountInfo);
} else {
replacementName =
- AccountTemplateUtil.getAccountTemplate(Iterables.getOnlyElement(possibleReplacements));
+ Optional.of(
+ AccountTemplateUtil.getAccountTemplate(
+ Iterables.getOnlyElement(possibleReplacements.keySet())));
}
return replacementName;
}
@@ -1135,6 +1226,13 @@
* recent update.
*/
private static class ChangeFixProgress {
+
+ /** {@link RefNames#changeMetaRef} of the change that is being fixed. */
+ final String changeMetaRef;
+
+ /** Tag at current commit update. */
+ String tag = null;
+
/** Assignee at current commit update. */
Account.Id assigneeId = null;
@@ -1146,7 +1244,7 @@
* #accountCache} if needed by rewrite. Maps to empty string if was not requested from cache
* yet.
*/
- Map<Account.Id, String> parsedAccounts = new HashMap<>();
+ Map<Account.Id, Optional<AccountState>> parsedAccounts = new HashMap<>();
/** Id of the current commit in rewriter walk. */
ObjectId newTipId = null;
@@ -1160,5 +1258,29 @@
boolean isValidAfterFix = true;
List<CommitDiff> commitDiffs = new ArrayList<>();
+
+ public ChangeFixProgress(String changeMetaRef) {
+ this.changeMetaRef = changeMetaRef;
+ }
+ }
+
+ /**
+ * Account info parsed from {@link Account#getNameEmail}. See {@link
+ * #getAccountInfoFromNameEmail}.
+ */
+ @AutoValue
+ abstract static class ParsedAccountInfo {
+
+ static ParsedAccountInfo create(String fullName, String email) {
+ return new AutoValue_CommitRewriter_ParsedAccountInfo(fullName, Optional.ofNullable(email));
+ }
+
+ static ParsedAccountInfo create(String fullName) {
+ return new AutoValue_CommitRewriter_ParsedAccountInfo(fullName, Optional.empty());
+ }
+
+ abstract String name();
+
+ abstract Optional<String> email();
}
}
diff --git a/java/com/google/gerrit/server/notedb/StoreSubmitRequirementsOp.java b/java/com/google/gerrit/server/notedb/StoreSubmitRequirementsOp.java
index 57a3cd7..1a7d5af 100644
--- a/java/com/google/gerrit/server/notedb/StoreSubmitRequirementsOp.java
+++ b/java/com/google/gerrit/server/notedb/StoreSubmitRequirementsOp.java
@@ -50,7 +50,9 @@
// patchset to the user before it was merged.
ChangeData changeData = changeDataFactory.create(ctx.getProject(), ctx.getChange().getId());
ChangeUpdate update = ctx.getUpdate(ctx.getChange().currentPatchSetId());
- update.putSubmitRequirementResults(evaluator.evaluateAllRequirements(changeData).values());
+ // We do not want to store submit requirements in NoteDb for legacy submit records
+ update.putSubmitRequirementResults(
+ evaluator.evaluateAllRequirements(changeData, /* includeLegacy= */ false).values());
return !changeData.submitRequirements().isEmpty();
}
}
diff --git a/java/com/google/gerrit/server/patch/DiffExecutor.java b/java/com/google/gerrit/server/patch/DiffExecutor.java
index 63d5c50..c9b87ff 100644
--- a/java/com/google/gerrit/server/patch/DiffExecutor.java
+++ b/java/com/google/gerrit/server/patch/DiffExecutor.java
@@ -16,14 +16,11 @@
import static java.lang.annotation.RetentionPolicy.RUNTIME;
-import com.google.gerrit.server.patch.filediff.PatchListLoader;
import com.google.inject.BindingAnnotation;
import java.lang.annotation.Retention;
import java.util.concurrent.ExecutorService;
-/**
- * Marker on {@link ExecutorService} used by {@link IntraLineLoader} and {@link PatchListLoader}.
- */
+/** Marker on {@link ExecutorService} used by {@link IntraLineLoader}. */
@Retention(RUNTIME)
@BindingAnnotation
public @interface DiffExecutor {}
diff --git a/java/com/google/gerrit/server/patch/PatchListCache.java b/java/com/google/gerrit/server/patch/PatchListCache.java
index 6d42249..b8651e0 100644
--- a/java/com/google/gerrit/server/patch/PatchListCache.java
+++ b/java/com/google/gerrit/server/patch/PatchListCache.java
@@ -16,18 +16,11 @@
import com.google.gerrit.entities.Project;
-/** Provides a cached list of {@link PatchListEntry}. */
+/**
+ * Provides a cached list of intra-line and summary diffs. Use {@link DiffOperations} to compute
+ * detailed file diffs.
+ */
public interface PatchListCache {
- /**
- * Returns the patch list - list of modified files - between two commits.
- *
- * @param key identifies the old / new commits.
- * @param project name key identifying a specific git project (repository).
- * @return patch list containing the modified files between two commits.
- * @deprecated use {@link DiffOperations} instead.
- */
- @Deprecated
- PatchList get(PatchListKey key, Project.NameKey project) throws PatchListNotAvailableException;
IntraLineDiff getIntraLineDiff(IntraLineDiffKey key, IntraLineDiffArgs args);
diff --git a/java/com/google/gerrit/server/patch/PatchListCacheImpl.java b/java/com/google/gerrit/server/patch/PatchListCacheImpl.java
index dd2bb47..eab0c22 100644
--- a/java/com/google/gerrit/server/patch/PatchListCacheImpl.java
+++ b/java/com/google/gerrit/server/patch/PatchListCacheImpl.java
@@ -15,14 +15,12 @@
package com.google.gerrit.server.patch;
-import com.google.common.annotations.VisibleForTesting;
import com.google.common.cache.Cache;
+import com.google.common.flogger.FluentLogger;
import com.google.common.util.concurrent.UncheckedExecutionException;
import com.google.gerrit.entities.Project;
-import com.google.gerrit.server.cache.CacheBackend;
import com.google.gerrit.server.cache.CacheModule;
import com.google.gerrit.server.config.GerritServerConfig;
-import com.google.gerrit.server.patch.filediff.PatchListLoader;
import com.google.inject.Inject;
import com.google.inject.Module;
import com.google.inject.Singleton;
@@ -30,12 +28,12 @@
import java.util.concurrent.ExecutionException;
import org.eclipse.jgit.errors.LargeObjectException;
import org.eclipse.jgit.lib.Config;
-import org.eclipse.jgit.lib.ObjectId;
/** Provides a cached list of {@link PatchListEntry}. */
@Singleton
public class PatchListCacheImpl implements PatchListCache {
- public static final String FILE_NAME = "diff";
+ public static final FluentLogger logger = FluentLogger.forEnclosingClass();
+
static final String INTRA_NAME = "diff_intraline";
static final String DIFF_SUMMARY = "diff_summary";
@@ -43,13 +41,6 @@
return new CacheModule() {
@Override
protected void configure() {
- factory(PatchListLoader.Factory.class);
- // TODO(davido): Switch off using legacy cache backend, after fixing PatchListLoader
- // to be recursion free.
- persist(FILE_NAME, PatchListKey.class, PatchList.class, CacheBackend.GUAVA)
- .maximumWeight(10 << 20)
- .weigher(PatchListWeigher.class);
-
factory(IntraLineLoader.Factory.class);
persist(INTRA_NAME, IntraLineDiffKey.class, IntraLineDiff.class)
.maximumWeight(10 << 20)
@@ -67,27 +58,21 @@
};
}
- private final Cache<PatchListKey, PatchList> fileCache;
private final Cache<IntraLineDiffKey, IntraLineDiff> intraCache;
private final Cache<DiffSummaryKey, DiffSummary> diffSummaryCache;
- private final PatchListLoader.Factory fileLoaderFactory;
private final IntraLineLoader.Factory intraLoaderFactory;
private final DiffSummaryLoader.Factory diffSummaryLoaderFactory;
private final boolean computeIntraline;
@Inject
PatchListCacheImpl(
- @Named(FILE_NAME) Cache<PatchListKey, PatchList> fileCache,
@Named(INTRA_NAME) Cache<IntraLineDiffKey, IntraLineDiff> intraCache,
@Named(DIFF_SUMMARY) Cache<DiffSummaryKey, DiffSummary> diffSummaryCache,
- PatchListLoader.Factory fileLoaderFactory,
IntraLineLoader.Factory intraLoaderFactory,
DiffSummaryLoader.Factory diffSummaryLoaderFactory,
@GerritServerConfig Config cfg) {
- this.fileCache = fileCache;
this.intraCache = intraCache;
this.diffSummaryCache = diffSummaryCache;
- this.fileLoaderFactory = fileLoaderFactory;
this.intraLoaderFactory = intraLoaderFactory;
this.diffSummaryLoaderFactory = diffSummaryLoaderFactory;
@@ -97,31 +82,6 @@
}
@Override
- public PatchList get(PatchListKey key, Project.NameKey project)
- throws PatchListNotAvailableException {
- try {
- PatchList pl = fileCache.get(key, fileLoaderFactory.create(key, project));
- if (pl instanceof LargeObjectTombstone) {
- throw new PatchListObjectTooLargeException(
- "Error computing " + key + ". Previous attempt failed with LargeObjectException");
- }
- return pl;
- } catch (ExecutionException e) {
- PatchListLoader.logger.atWarning().withCause(e).log("Error computing %s", key);
- throw new PatchListNotAvailableException(e);
- } catch (UncheckedExecutionException e) {
- if (e.getCause() instanceof LargeObjectException) {
- // Cache negative result so we don't need to redo expensive computations that would yield
- // the same result.
- fileCache.put(key, new LargeObjectTombstone());
- PatchListLoader.logger.atWarning().withCause(e).log("Error computing %s", key);
- throw new PatchListNotAvailableException(e);
- }
- throw e;
- }
- }
-
- @Override
public IntraLineDiff getIntraLineDiff(IntraLineDiffKey key, IntraLineDiffArgs args) {
if (computeIntraline) {
try {
@@ -140,28 +100,14 @@
try {
return diffSummaryCache.get(key, diffSummaryLoaderFactory.create(key, project));
} catch (ExecutionException e) {
- PatchListLoader.logger.atWarning().withCause(e).log("Error computing %s", key);
+ logger.atWarning().withCause(e).log("Error computing %s", key);
throw new PatchListNotAvailableException(e);
} catch (UncheckedExecutionException e) {
if (e.getCause() instanceof LargeObjectException) {
- PatchListLoader.logger.atWarning().withCause(e).log("Error computing %s", key);
+ logger.atWarning().withCause(e).log("Error computing %s", key);
throw new PatchListNotAvailableException(e);
}
throw e;
}
}
-
- /** Used to cache negative results in {@code fileCache}. */
- @VisibleForTesting
- public static class LargeObjectTombstone extends PatchList {
- private static final long serialVersionUID = 1L;
-
- @VisibleForTesting
- public LargeObjectTombstone() {
- // Initialize super class with valid values. We don't care about the inner state, but need to
- // pass valid values that don't break (de)serialization.
- super(
- null, ObjectId.zeroId(), false, ComparisonType.againstAutoMerge(), new PatchListEntry[0]);
- }
- }
}
diff --git a/java/com/google/gerrit/server/patch/PatchListWeigher.java b/java/com/google/gerrit/server/patch/PatchListWeigher.java
deleted file mode 100644
index 942d0e0..0000000
--- a/java/com/google/gerrit/server/patch/PatchListWeigher.java
+++ /dev/null
@@ -1,37 +0,0 @@
-// Copyright (C) 2012 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.patch;
-
-import com.google.common.cache.Weigher;
-
-/** Approximates memory usage for PatchList in bytes of memory used. */
-public class PatchListWeigher implements Weigher<PatchListKey, PatchList> {
- @Override
- public int weigh(PatchListKey key, PatchList value) {
- int size =
- 16
- + 4 * 8
- + 2 * 36
- + 8 // Size of PatchListKey, 64 bit JVM
- + 16
- + 3 * 8
- + 3 * 4
- + 20; // Size of PatchList, 64 bit JVM
- for (PatchListEntry e : value.getPatches()) {
- size += e.weigh();
- }
- return size;
- }
-}
diff --git a/java/com/google/gerrit/server/patch/PatchScriptBuilder.java b/java/com/google/gerrit/server/patch/PatchScriptBuilder.java
index 0b08c4f..33300e3 100644
--- a/java/com/google/gerrit/server/patch/PatchScriptBuilder.java
+++ b/java/com/google/gerrit/server/patch/PatchScriptBuilder.java
@@ -71,28 +71,8 @@
intralineDiffCalculator = calculator;
}
- /** Convert into {@link PatchScript} using the old diff cache output. */
- PatchScript toPatchScriptOld(Repository git, PatchList list, PatchListEntry content)
- throws IOException {
-
- PatchFileChange change =
- new PatchFileChange(
- content.getEdits(),
- content.getEditsDueToRebase(),
- content.getHeaderLines(),
- content.getOldName(),
- content.getNewName(),
- content.getChangeType(),
- content.getPatchType());
- SidesResolver sidesResolver = new SidesResolver(git, list.getComparisonType());
- ResolvedSides sides =
- resolveSides(
- git, sidesResolver, oldName(change), newName(change), list.getOldId(), list.getNewId());
- return build(sides.a, sides.b, change);
- }
-
/** Convert into {@link PatchScript} using the new diff cache output. */
- PatchScript toPatchScriptNew(Repository git, FileDiffOutput content) throws IOException {
+ PatchScript toPatchScript(Repository git, FileDiffOutput content) throws IOException {
PatchFileChange change =
new PatchFileChange(
content.edits().stream().map(TaggedEdit::jgitEdit).collect(toImmutableList()),
diff --git a/java/com/google/gerrit/server/patch/PatchScriptFactory.java b/java/com/google/gerrit/server/patch/PatchScriptFactory.java
index 96b23c8..02f125a 100644
--- a/java/com/google/gerrit/server/patch/PatchScriptFactory.java
+++ b/java/com/google/gerrit/server/patch/PatchScriptFactory.java
@@ -229,7 +229,7 @@
notes.getProjectName(), bId, parentNum, fileName, diffPrefs.ignoreWhitespace)
: diffOperations.getModifiedFile(
notes.getProjectName(), aId, bId, fileName, diffPrefs.ignoreWhitespace);
- return newBuilder().toPatchScriptNew(git, fileDiffOutput);
+ return newBuilder().toPatchScript(git, fileDiffOutput);
}
private Optional<ObjectId> getAId() {
diff --git a/java/com/google/gerrit/server/patch/filediff/PatchListLoader.java b/java/com/google/gerrit/server/patch/filediff/PatchListLoader.java
deleted file mode 100644
index 031f3db..0000000
--- a/java/com/google/gerrit/server/patch/filediff/PatchListLoader.java
+++ /dev/null
@@ -1,705 +0,0 @@
-// Copyright (C) 2020 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.patch.filediff;
-
-import static com.google.common.base.Preconditions.checkArgument;
-import static com.google.common.collect.ImmutableList.toImmutableList;
-import static java.nio.charset.StandardCharsets.UTF_8;
-import static java.util.stream.Collectors.toSet;
-import static org.eclipse.jgit.lib.Constants.OBJ_BLOB;
-
-import com.google.auto.value.AutoValue;
-import com.google.common.base.Throwables;
-import com.google.common.collect.ImmutableList;
-import com.google.common.collect.ImmutableMultimap;
-import com.google.common.collect.ImmutableSet;
-import com.google.common.collect.Iterables;
-import com.google.common.collect.Multimap;
-import com.google.common.flogger.FluentLogger;
-import com.google.gerrit.entities.Patch;
-import com.google.gerrit.entities.Project;
-import com.google.gerrit.exceptions.StorageException;
-import com.google.gerrit.extensions.client.DiffPreferencesInfo.Whitespace;
-import com.google.gerrit.metrics.Counter0;
-import com.google.gerrit.metrics.Description;
-import com.google.gerrit.metrics.MetricMaker;
-import com.google.gerrit.server.config.ConfigUtil;
-import com.google.gerrit.server.config.GerritServerConfig;
-import com.google.gerrit.server.git.GitRepositoryManager;
-import com.google.gerrit.server.git.InMemoryInserter;
-import com.google.gerrit.server.git.MergeUtil;
-import com.google.gerrit.server.patch.AutoMerger;
-import com.google.gerrit.server.patch.ComparisonType;
-import com.google.gerrit.server.patch.DiffExecutor;
-import com.google.gerrit.server.patch.PatchList;
-import com.google.gerrit.server.patch.PatchListCache;
-import com.google.gerrit.server.patch.PatchListCacheImpl;
-import com.google.gerrit.server.patch.PatchListEntry;
-import com.google.gerrit.server.patch.PatchListKey;
-import com.google.gerrit.server.patch.PatchListNotAvailableException;
-import com.google.gerrit.server.patch.Text;
-import com.google.gerrit.server.patch.filediff.EditTransformer.ContextAwareEdit;
-import com.google.inject.Inject;
-import com.google.inject.Singleton;
-import com.google.inject.assistedinject.Assisted;
-import java.io.IOException;
-import java.util.ArrayList;
-import java.util.Collection;
-import java.util.HashSet;
-import java.util.List;
-import java.util.Optional;
-import java.util.Set;
-import java.util.concurrent.Callable;
-import java.util.concurrent.ExecutionException;
-import java.util.concurrent.ExecutorService;
-import java.util.concurrent.Future;
-import java.util.concurrent.TimeUnit;
-import java.util.concurrent.TimeoutException;
-import org.eclipse.jgit.diff.DiffEntry;
-import org.eclipse.jgit.diff.DiffEntry.ChangeType;
-import org.eclipse.jgit.diff.DiffFormatter;
-import org.eclipse.jgit.diff.Edit;
-import org.eclipse.jgit.diff.EditList;
-import org.eclipse.jgit.diff.HistogramDiff;
-import org.eclipse.jgit.diff.RawText;
-import org.eclipse.jgit.diff.RawTextComparator;
-import org.eclipse.jgit.lib.AbbreviatedObjectId;
-import org.eclipse.jgit.lib.Config;
-import org.eclipse.jgit.lib.Constants;
-import org.eclipse.jgit.lib.FileMode;
-import org.eclipse.jgit.lib.ObjectId;
-import org.eclipse.jgit.lib.ObjectInserter;
-import org.eclipse.jgit.lib.ObjectReader;
-import org.eclipse.jgit.lib.Repository;
-import org.eclipse.jgit.merge.ThreeWayMergeStrategy;
-import org.eclipse.jgit.patch.FileHeader;
-import org.eclipse.jgit.patch.FileHeader.PatchType;
-import org.eclipse.jgit.revwalk.RevCommit;
-import org.eclipse.jgit.revwalk.RevObject;
-import org.eclipse.jgit.revwalk.RevTree;
-import org.eclipse.jgit.revwalk.RevWalk;
-import org.eclipse.jgit.treewalk.TreeWalk;
-import org.eclipse.jgit.util.io.DisabledOutputStream;
-
-public class PatchListLoader implements Callable<PatchList> {
- public static final FluentLogger logger = FluentLogger.forEnclosingClass();
-
- public interface Factory {
- PatchListLoader create(PatchListKey key, Project.NameKey project);
- }
-
- @Singleton
- static class Metrics {
- final Counter0 timeouts;
-
- @Inject
- Metrics(MetricMaker metricMaker) {
- // TODO(ghareeb): Remove this metric from documentation once this class is deprecated.
- timeouts =
- metricMaker.newCounter(
- "caches/diff/legacy/timeouts",
- new Description(
- "Total number of git file diff computations that resulted in timeouts.")
- .setRate()
- .setUnit("count"));
- }
- }
-
- private final GitRepositoryManager repoManager;
- private final PatchListCache patchListCache;
- private final ThreeWayMergeStrategy mergeStrategy;
- private final ExecutorService diffExecutor;
- private final AutoMerger autoMerger;
- private final PatchListKey key;
- private final Metrics metrics;
- private final Project.NameKey project;
- private final long timeoutMillis;
-
- @Inject
- PatchListLoader(
- GitRepositoryManager mgr,
- PatchListCache plc,
- @GerritServerConfig Config cfg,
- @DiffExecutor ExecutorService de,
- AutoMerger am,
- Metrics metrics,
- @Assisted PatchListKey k,
- @Assisted Project.NameKey p) {
- this.repoManager = mgr;
- this.patchListCache = plc;
- this.mergeStrategy = MergeUtil.getMergeStrategy(cfg);
- this.diffExecutor = de;
- this.autoMerger = am;
- this.metrics = metrics;
- this.key = k;
- this.project = p;
- this.timeoutMillis =
- ConfigUtil.getTimeUnit(
- cfg,
- "cache",
- PatchListCacheImpl.FILE_NAME,
- "timeout",
- TimeUnit.MILLISECONDS.convert(5, TimeUnit.SECONDS),
- TimeUnit.MILLISECONDS);
- }
-
- @Override
- public PatchList call() throws IOException, PatchListNotAvailableException {
- try (Repository repo = repoManager.openRepository(project);
- InMemoryInserter ins = new InMemoryInserter(repo);
- ObjectReader reader = ins.newReader();
- RevWalk rw = new RevWalk(reader)) {
- return readPatchList(repo, rw, ins);
- }
- }
-
- private static RawTextComparator comparatorFor(Whitespace ws) {
- switch (ws) {
- case IGNORE_ALL:
- return RawTextComparator.WS_IGNORE_ALL;
-
- case IGNORE_TRAILING:
- return RawTextComparator.WS_IGNORE_TRAILING;
-
- case IGNORE_LEADING_AND_TRAILING:
- return RawTextComparator.WS_IGNORE_CHANGE;
-
- case IGNORE_NONE:
- default:
- return RawTextComparator.DEFAULT;
- }
- }
-
- private PatchList readPatchList(Repository repo, RevWalk rw, InMemoryInserter ins)
- throws IOException, PatchListNotAvailableException {
- ObjectReader reader = rw.getObjectReader();
- checkArgument(reader.getCreatedFromInserter() == ins);
- RawTextComparator cmp = comparatorFor(key.getWhitespace());
- try (DiffFormatter df = new DiffFormatter(DisabledOutputStream.INSTANCE)) {
- RevCommit b = rw.parseCommit(key.getNewId());
- RevObject a = aFor(key, repo, rw, ins, b);
-
- if (a == null) {
- // TODO(sop) Remove this case.
- // This is an octopus merge commit which should be compared against the
- // auto-merge. However since we don't support computing the auto-merge
- // for octopus merge commits, we fall back to diffing against the first
- // parent, even though this wasn't what was requested.
- //
- ComparisonType comparisonType = ComparisonType.againstParent(1);
- PatchListEntry[] entries = new PatchListEntry[2];
- entries[0] = newCommitMessage(cmp, reader, null, b);
- entries[1] = newMergeList(cmp, reader, null, b, comparisonType);
- return new PatchList(a, b, true, comparisonType, entries);
- }
-
- ComparisonType comparisonType = getComparisonType(a, b);
-
- RevCommit aCommit = a instanceof RevCommit ? (RevCommit) a : null;
- RevTree aTree = rw.parseTree(a);
- RevTree bTree = b.getTree();
-
- df.setReader(reader, repo.getConfig());
- df.setDiffComparator(cmp);
- df.setDetectRenames(true);
- List<DiffEntry> diffEntries = df.scan(aTree, bTree);
-
- EditsDueToRebaseResult editsDueToRebaseResult =
- determineEditsDueToRebase(aCommit, b, diffEntries, df, rw);
- diffEntries = editsDueToRebaseResult.getRelevantOriginalDiffEntries();
- Multimap<String, ContextAwareEdit> editsDueToRebasePerFilePath =
- editsDueToRebaseResult.getEditsDueToRebasePerFilePath();
-
- List<PatchListEntry> entries = new ArrayList<>();
- entries.add(
- newCommitMessage(
- cmp, reader, comparisonType.isAgainstParentOrAutoMerge() ? null : aCommit, b));
- boolean isMerge = b.getParentCount() > 1;
- if (isMerge) {
- entries.add(
- newMergeList(
- cmp,
- reader,
- comparisonType.isAgainstParentOrAutoMerge() ? null : aCommit,
- b,
- comparisonType));
- }
- for (DiffEntry diffEntry : diffEntries) {
- Set<ContextAwareEdit> editsDueToRebase =
- getEditsDueToRebase(editsDueToRebasePerFilePath, diffEntry);
- Optional<PatchListEntry> patchListEntry =
- getPatchListEntry(reader, df, diffEntry, aTree, bTree, editsDueToRebase);
- patchListEntry.ifPresent(entries::add);
- }
- return new PatchList(
- a, b, isMerge, comparisonType, entries.toArray(new PatchListEntry[entries.size()]));
- }
- }
-
- /**
- * Identifies the edits which are present between {@code commitA} and {@code commitB} due to other
- * commits in between those two. Edits which cannot be clearly attributed to those other commits
- * (because they overlap with modifications introduced by {@code commitA} or {@code commitB}) are
- * omitted from the result. The edits are expressed as differences between {@code treeA} of {@code
- * commitA} and {@code treeB} of {@code commitB}.
- *
- * <p><b>Note:</b> If one of the commits is a merge commit, an empty {@code Multimap} will be
- * returned.
- *
- * <p><b>Warning:</b> This method assumes that commitA and commitB are either a parent and child
- * commit or represent two patch sets which belong to the same change. No checks are made to
- * confirm this assumption! Passing arbitrary commits to this method may lead to strange results
- * or take very long.
- *
- * <p>This logic could be expanded to arbitrary commits if the following adjustments were applied:
- *
- * <ul>
- * <li>If {@code commitA} is an ancestor of {@code commitB} (or the other way around), {@code
- * commitA} (or {@code commitB}) is used instead of its parent in this method.
- * <li>Special handling for merge commits is added. If only one of them is a merge commit, the
- * whole computation has to be done between the single parent and all parents of the merge
- * commit. If both of them are merge commits, all combinations of parents have to be
- * considered. Alternatively, we could decide to not support this feature for merge commits
- * (or just for specific types of merge commits).
- * </ul>
- *
- * @param commitA the commit defining {@code treeA}
- * @param commitB the commit defining {@code treeB}
- * @param diffEntries the list of {@code DiffEntries} for the diff between {@code commitA} and
- * {@code commitB}
- * @param df the {@code DiffFormatter}
- * @param rw the current {@code RevWalk}
- * @return an aggregated result of the computation
- * @throws PatchListNotAvailableException if the edits can't be identified
- * @throws IOException if an error occurred while accessing the repository
- */
- private EditsDueToRebaseResult determineEditsDueToRebase(
- RevCommit commitA,
- RevCommit commitB,
- List<DiffEntry> diffEntries,
- DiffFormatter df,
- RevWalk rw)
- throws PatchListNotAvailableException, IOException {
- if (commitA == null
- || isRootOrMergeCommit(commitA)
- || isRootOrMergeCommit(commitB)
- || areParentChild(commitA, commitB)
- || haveCommonParent(commitA, commitB)) {
- return EditsDueToRebaseResult.create(diffEntries, ImmutableMultimap.of());
- }
-
- PatchListKey oldKey = PatchListKey.againstDefaultBase(key.getOldId(), key.getWhitespace());
- PatchList oldPatchList = patchListCache.get(oldKey, project);
- PatchListKey newKey = PatchListKey.againstDefaultBase(key.getNewId(), key.getWhitespace());
- PatchList newPatchList = patchListCache.get(newKey, project);
-
- List<PatchListEntry> oldPatches = oldPatchList.getPatches();
- List<PatchListEntry> newPatches = newPatchList.getPatches();
- // TODO(aliceks): Have separate but more limited lists for parents and patch sets (but don't
- // mess up renames/copies).
- Set<String> touchedFilePaths = new HashSet<>();
- for (PatchListEntry patchListEntry : oldPatches) {
- touchedFilePaths.addAll(getTouchedFilePaths(patchListEntry));
- }
- for (PatchListEntry patchListEntry : newPatches) {
- touchedFilePaths.addAll(getTouchedFilePaths(patchListEntry));
- }
-
- List<DiffEntry> relevantDiffEntries =
- diffEntries.stream()
- .filter(diffEntry -> isTouched(touchedFilePaths, diffEntry))
- .collect(toImmutableList());
-
- RevCommit parentCommitA = commitA.getParent(0);
- rw.parseBody(parentCommitA);
- RevCommit parentCommitB = commitB.getParent(0);
- rw.parseBody(parentCommitB);
- List<DiffEntry> parentDiffEntries = df.scan(parentCommitA, parentCommitB);
- // TODO(aliceks): Find a way to not construct a PatchListEntry as it contains many unnecessary
- // details and we don't fill all of them properly.
- List<PatchListEntry> parentPatchListEntries =
- getRelevantPatchListEntries(
- parentDiffEntries, parentCommitA, parentCommitB, touchedFilePaths, df);
-
- EditTransformer editTransformer = new EditTransformer(toFileEditsList(parentPatchListEntries));
- editTransformer.transformReferencesOfSideA(toFileEditsList(oldPatches));
- editTransformer.transformReferencesOfSideB(toFileEditsList(newPatches));
- return EditsDueToRebaseResult.create(
- relevantDiffEntries, editTransformer.getEditsPerFilePath());
- }
-
- private ImmutableList<FileEdits> toFileEditsList(List<PatchListEntry> entries) {
- return entries.stream().map(PatchListLoader::toFileEdits).collect(toImmutableList());
- }
-
- private static FileEdits toFileEdits(PatchListEntry patchListEntry) {
- Optional<String> oldName = Optional.empty();
- Optional<String> newName = Optional.empty();
- switch (patchListEntry.getChangeType()) {
- case DELETED:
- oldName = Optional.of(patchListEntry.getNewName());
- break;
- case ADDED:
- case MODIFIED:
- case REWRITE:
- newName = Optional.of(patchListEntry.getNewName());
- break;
-
- case COPIED:
- case RENAMED:
- oldName = Optional.of(patchListEntry.getOldName());
- newName = Optional.of(patchListEntry.getNewName());
- break;
- }
- return FileEdits.createFromJgitEdits(patchListEntry.getEdits(), oldName, newName);
- }
-
- private static boolean isRootOrMergeCommit(RevCommit commit) {
- return commit.getParentCount() != 1;
- }
-
- private static boolean areParentChild(RevCommit commitA, RevCommit commitB) {
- return ObjectId.isEqual(commitA.getParent(0), commitB)
- || ObjectId.isEqual(commitB.getParent(0), commitA);
- }
-
- private static boolean haveCommonParent(RevCommit commitA, RevCommit commitB) {
- return ObjectId.isEqual(commitA.getParent(0), commitB.getParent(0));
- }
-
- private static Set<String> getTouchedFilePaths(PatchListEntry patchListEntry) {
- String oldFilePath = patchListEntry.getOldName();
- String newFilePath = patchListEntry.getNewName();
-
- return oldFilePath == null
- ? ImmutableSet.of(newFilePath)
- : ImmutableSet.of(oldFilePath, newFilePath);
- }
-
- private static boolean isTouched(Set<String> touchedFilePaths, DiffEntry diffEntry) {
- String oldFilePath = diffEntry.getOldPath();
- String newFilePath = diffEntry.getNewPath();
- // One of the above file paths could be /dev/null but we need not explicitly check for this
- // value as the set of file paths shouldn't contain it.
- return touchedFilePaths.contains(oldFilePath) || touchedFilePaths.contains(newFilePath);
- }
-
- private List<PatchListEntry> getRelevantPatchListEntries(
- List<DiffEntry> parentDiffEntries,
- RevCommit parentCommitA,
- RevCommit parentCommitB,
- Set<String> touchedFilePaths,
- DiffFormatter diffFormatter)
- throws IOException {
- List<PatchListEntry> parentPatchListEntries = new ArrayList<>(parentDiffEntries.size());
- for (DiffEntry parentDiffEntry : parentDiffEntries) {
- if (!isTouched(touchedFilePaths, parentDiffEntry)) {
- continue;
- }
- FileHeader fileHeader = toFileHeader(parentCommitB, diffFormatter, parentDiffEntry);
- // The code which uses this PatchListEntry doesn't care about the last three parameters. As
- // they are expensive to compute, we use arbitrary values for them.
- PatchListEntry patchListEntry =
- newEntry(parentCommitA.getTree(), fileHeader, ImmutableSet.of(), 0, 0);
- parentPatchListEntries.add(patchListEntry);
- }
- return parentPatchListEntries;
- }
-
- private static Set<ContextAwareEdit> getEditsDueToRebase(
- Multimap<String, ContextAwareEdit> editsDueToRebasePerFilePath, DiffEntry diffEntry) {
- if (editsDueToRebasePerFilePath.isEmpty()) {
- return ImmutableSet.of();
- }
-
- String filePath = diffEntry.getNewPath();
- if (diffEntry.getChangeType() == ChangeType.DELETE) {
- filePath = diffEntry.getOldPath();
- }
- return ImmutableSet.copyOf(editsDueToRebasePerFilePath.get(filePath));
- }
-
- private Optional<PatchListEntry> getPatchListEntry(
- ObjectReader objectReader,
- DiffFormatter diffFormatter,
- DiffEntry diffEntry,
- RevTree treeA,
- RevTree treeB,
- Set<ContextAwareEdit> editsDueToRebase)
- throws IOException {
- FileHeader fileHeader = toFileHeader(key.getNewId(), diffFormatter, diffEntry);
- long oldSize =
- getFileSize(
- objectReader,
- diffEntry.getOldId(),
- diffEntry.getOldMode(),
- diffEntry.getOldPath(),
- treeA);
- long newSize =
- getFileSize(
- objectReader,
- diffEntry.getNewId(),
- diffEntry.getNewMode(),
- diffEntry.getNewPath(),
- treeB);
- Set<Edit> contentEditsDueToRebase = getContentEdits(editsDueToRebase);
- PatchListEntry patchListEntry =
- newEntry(treeA, fileHeader, contentEditsDueToRebase, newSize, newSize - oldSize);
- // All edits in a file are due to rebase -> exclude the file from the diff.
- if (EditTransformer.toEdits(toFileEdits(patchListEntry)).allMatch(editsDueToRebase::contains)) {
- return Optional.empty();
- }
- return Optional.of(patchListEntry);
- }
-
- private static Set<Edit> getContentEdits(Set<ContextAwareEdit> editsDueToRebase) {
- return editsDueToRebase.stream()
- .map(ContextAwareEdit::toEdit)
- .filter(Optional::isPresent)
- .map(Optional::get)
- .collect(toSet());
- }
-
- private ComparisonType getComparisonType(RevObject a, RevCommit b) {
- for (int i = 0; i < b.getParentCount(); i++) {
- if (b.getParent(i).equals(a)) {
- return ComparisonType.againstParent(i + 1);
- }
- }
-
- if (key.getOldId() == null && b.getParentCount() > 0) {
- return ComparisonType.againstAutoMerge();
- }
-
- return ComparisonType.againstOtherPatchSet();
- }
-
- private static long getFileSize(
- ObjectReader reader, AbbreviatedObjectId abbreviatedId, FileMode mode, String path, RevTree t)
- throws IOException {
- if (!isBlob(mode)) {
- return 0;
- }
- ObjectId fileId =
- toObjectId(reader, abbreviatedId).orElseGet(() -> lookupObjectId(reader, path, t));
- if (ObjectId.zeroId().equals(fileId)) {
- return 0;
- }
- return reader.getObjectSize(fileId, OBJ_BLOB);
- }
-
- private static boolean isBlob(FileMode mode) {
- int t = mode.getBits() & FileMode.TYPE_MASK;
- return t == FileMode.TYPE_FILE || t == FileMode.TYPE_SYMLINK;
- }
-
- private static Optional<ObjectId> toObjectId(
- ObjectReader reader, AbbreviatedObjectId abbreviatedId) throws IOException {
- if (abbreviatedId == null) {
- // In theory, DiffEntry#getOldId or DiffEntry#getNewId can be null for pure renames or pure
- // mode changes (e.g. DiffEntry#modify doesn't set the IDs). However, the method we call
- // for diffs (DiffFormatter#scan) seems to always produce DiffEntries with set IDs, even for
- // pure renames.
- return Optional.empty();
- }
- if (abbreviatedId.isComplete()) {
- // With the current JGit version and the method we call for diffs (DiffFormatter#scan), this
- // is the only code path taken right now.
- return Optional.ofNullable(abbreviatedId.toObjectId());
- }
- Collection<ObjectId> objectIds = reader.resolve(abbreviatedId);
- // It seems very unlikely that an ObjectId which was just abbreviated by the diff computation
- // now can't be resolved to exactly one ObjectId. The API allows this possibility, though.
- return objectIds.size() == 1
- ? Optional.of(Iterables.getOnlyElement(objectIds))
- : Optional.empty();
- }
-
- private static ObjectId lookupObjectId(ObjectReader reader, String path, RevTree tree) {
- // This variant is very expensive.
- try (TreeWalk treeWalk = TreeWalk.forPath(reader, path, tree)) {
- return treeWalk != null ? treeWalk.getObjectId(0) : ObjectId.zeroId();
- } catch (IOException e) {
- throw new StorageException(e);
- }
- }
-
- private FileHeader toFileHeader(
- ObjectId commitB, DiffFormatter diffFormatter, DiffEntry diffEntry) throws IOException {
-
- Future<FileHeader> result =
- diffExecutor.submit(
- () -> {
- synchronized (diffEntry) {
- return diffFormatter.toFileHeader(diffEntry);
- }
- });
-
- try {
- return result.get(timeoutMillis, TimeUnit.MILLISECONDS);
- } catch (InterruptedException | TimeoutException e) {
- metrics.timeouts.increment();
- logger.atWarning().log(
- "%s ms timeout reached for Diff loader in project %s"
- + " on commit %s on path %s comparing %s..%s",
- timeoutMillis,
- project,
- commitB.name(),
- diffEntry.getNewPath(),
- diffEntry.getOldId().name(),
- diffEntry.getNewId().name());
- result.cancel(true);
- synchronized (diffEntry) {
- return toFileHeaderWithoutMyersDiff(diffFormatter, diffEntry);
- }
- } catch (ExecutionException e) {
- // If there was an error computing the result, carry it
- // up to the caller so the cache knows this key is invalid.
- Throwables.throwIfInstanceOf(e.getCause(), IOException.class);
- throw new IOException(e.getMessage(), e.getCause());
- }
- }
-
- private FileHeader toFileHeaderWithoutMyersDiff(DiffFormatter diffFormatter, DiffEntry diffEntry)
- throws IOException {
- HistogramDiff histogramDiff = new HistogramDiff();
- histogramDiff.setFallbackAlgorithm(null);
- diffFormatter.setDiffAlgorithm(histogramDiff);
- return diffFormatter.toFileHeader(diffEntry);
- }
-
- private PatchListEntry newCommitMessage(
- RawTextComparator cmp, ObjectReader reader, RevCommit aCommit, RevCommit bCommit)
- throws IOException {
- Text aText = aCommit != null ? Text.forCommit(reader, aCommit) : Text.EMPTY;
- Text bText = Text.forCommit(reader, bCommit);
- return createPatchListEntry(cmp, aCommit, aText, bText, Patch.COMMIT_MSG);
- }
-
- private PatchListEntry newMergeList(
- RawTextComparator cmp,
- ObjectReader reader,
- RevCommit aCommit,
- RevCommit bCommit,
- ComparisonType comparisonType)
- throws IOException {
- Text aText = aCommit != null ? Text.forMergeList(comparisonType, reader, aCommit) : Text.EMPTY;
- Text bText = Text.forMergeList(comparisonType, reader, bCommit);
- return createPatchListEntry(cmp, aCommit, aText, bText, Patch.MERGE_LIST);
- }
-
- private static PatchListEntry createPatchListEntry(
- RawTextComparator cmp, RevCommit aCommit, Text aText, Text bText, String fileName) {
- byte[] rawHdr = getRawHeader(aCommit != null, fileName);
- byte[] aContent = aText.getContent();
- byte[] bContent = bText.getContent();
- long size = bContent.length;
- long sizeDelta = size - aContent.length;
- RawText aRawText = new RawText(aContent);
- RawText bRawText = new RawText(bContent);
- EditList edits = new HistogramDiff().diff(cmp, aRawText, bRawText);
- FileHeader fh = new FileHeader(rawHdr, edits, PatchType.UNIFIED);
- return new PatchListEntry(fh, edits, ImmutableSet.of(), size, sizeDelta);
- }
-
- private static byte[] getRawHeader(boolean hasA, String fileName) {
- StringBuilder hdr = new StringBuilder();
- hdr.append("diff --git");
- if (hasA) {
- hdr.append(" a/").append(fileName);
- } else {
- hdr.append(" ").append(FileHeader.DEV_NULL);
- }
- hdr.append(" b/").append(fileName);
- hdr.append("\n");
-
- if (hasA) {
- hdr.append("--- a/").append(fileName).append("\n");
- } else {
- hdr.append("--- ").append(FileHeader.DEV_NULL).append("\n");
- }
- hdr.append("+++ b/").append(fileName).append("\n");
- return hdr.toString().getBytes(UTF_8);
- }
-
- private static PatchListEntry newEntry(
- RevTree aTree, FileHeader fileHeader, Set<Edit> editsDueToRebase, long size, long sizeDelta) {
- if (aTree == null // want combined diff
- || fileHeader.getPatchType() != PatchType.UNIFIED
- || fileHeader.getHunks().isEmpty()) {
- return new PatchListEntry(fileHeader, ImmutableList.of(), ImmutableSet.of(), size, sizeDelta);
- }
-
- List<Edit> edits = fileHeader.toEditList();
- if (edits.isEmpty()) {
- return new PatchListEntry(fileHeader, ImmutableList.of(), ImmutableSet.of(), size, sizeDelta);
- }
- return new PatchListEntry(fileHeader, edits, editsDueToRebase, size, sizeDelta);
- }
-
- private RevObject aFor(
- PatchListKey key, Repository repo, RevWalk rw, InMemoryInserter ins, RevCommit b)
- throws IOException {
- if (key.getOldId() != null) {
- return rw.parseAny(key.getOldId());
- }
-
- switch (b.getParentCount()) {
- case 0:
- return rw.parseAny(emptyTree(ins));
- case 1:
- {
- RevCommit r = b.getParent(0);
- rw.parseBody(r);
- return r;
- }
- default:
- if (key.getParentNum() != null) {
- RevCommit r = b.getParent(key.getParentNum() - 1);
- rw.parseBody(r);
- return r;
- }
- // Only support auto-merge for 2 parents, not octopus merges
- if (b.getParentCount() == 2) {
- return autoMerger.lookupFromGitOrMergeInMemory(repo, rw, ins, b, mergeStrategy);
- }
- return null;
- }
- }
-
- private static ObjectId emptyTree(ObjectInserter ins) throws IOException {
- ObjectId id = ins.insert(Constants.OBJ_TREE, new byte[] {});
- ins.flush();
- return id;
- }
-
- @AutoValue
- abstract static class EditsDueToRebaseResult {
- public static EditsDueToRebaseResult create(
- List<DiffEntry> relevantDiffEntries,
- Multimap<String, ContextAwareEdit> editsDueToRebasePerFilePath) {
- return new AutoValue_PatchListLoader_EditsDueToRebaseResult(
- relevantDiffEntries, editsDueToRebasePerFilePath);
- }
-
- public abstract List<DiffEntry> getRelevantOriginalDiffEntries();
-
- /** Returns the edits per file path they modify in {@code treeB}. */
- public abstract Multimap<String, ContextAwareEdit> getEditsDueToRebasePerFilePath();
- }
-}
diff --git a/java/com/google/gerrit/server/project/SubmitRequirementsAdapter.java b/java/com/google/gerrit/server/project/SubmitRequirementsAdapter.java
index f028def..63a29cc 100644
--- a/java/com/google/gerrit/server/project/SubmitRequirementsAdapter.java
+++ b/java/com/google/gerrit/server/project/SubmitRequirementsAdapter.java
@@ -25,8 +25,12 @@
import com.google.gerrit.entities.SubmitRequirementExpressionResult;
import com.google.gerrit.entities.SubmitRequirementExpressionResult.Status;
import com.google.gerrit.entities.SubmitRequirementResult;
+import com.google.gerrit.server.query.change.ChangeData;
import com.google.gerrit.server.query.change.ChangeQueryBuilder;
import java.util.List;
+import java.util.Map;
+import java.util.function.Function;
+import java.util.stream.Collectors;
import org.eclipse.jgit.lib.ObjectId;
/**
@@ -38,7 +42,25 @@
private SubmitRequirementsAdapter() {}
- public static List<SubmitRequirementResult> createResult(
+ /**
+ * Retrieve legacy submit records (created by label functions and other {@link
+ * com.google.gerrit.server.rules.SubmitRule}s) and convert them to submit requirement results.
+ */
+ public static Map<SubmitRequirement, SubmitRequirementResult> getLegacyRequirements(
+ SubmitRuleEvaluator.Factory evaluator, ChangeData cd) {
+ // We use SubmitRuleOptions.defaults() which does not recompute submit rules for closed changes.
+ // This doesn't have an effect since we never call this class (i.e. to evaluate submit
+ // requirements) for closed changes.
+ List<SubmitRecord> records = evaluator.create(SubmitRuleOptions.defaults()).evaluate(cd);
+ List<LabelType> labelTypes = cd.getLabelTypes().getLabelTypes();
+ ObjectId commitId = cd.currentPatchSet().commitId();
+ return records.stream()
+ .map(r -> createResult(r, labelTypes, commitId))
+ .flatMap(List::stream)
+ .collect(Collectors.toMap(sr -> sr.submitRequirement(), Function.identity()));
+ }
+
+ static List<SubmitRequirementResult> createResult(
SubmitRecord record, List<LabelType> labelTypes, ObjectId psCommitId) {
List<SubmitRequirementResult> results;
if (record.ruleName.equals("gerrit~DefaultSubmitRule")) {
diff --git a/java/com/google/gerrit/server/project/SubmitRequirementsEvaluator.java b/java/com/google/gerrit/server/project/SubmitRequirementsEvaluator.java
index b3ac380..402bb51 100644
--- a/java/com/google/gerrit/server/project/SubmitRequirementsEvaluator.java
+++ b/java/com/google/gerrit/server/project/SubmitRequirementsEvaluator.java
@@ -26,8 +26,13 @@
/**
* Evaluate and return all submit requirement results for a change. Submit requirements are read
* from the project config of the project containing the change as well as parent projects.
+ *
+ * @param cd change data corresponding to a specific gerrit change
+ * @param includeLegacy if set to true, evaluate legacy {@link
+ * com.google.gerrit.entities.SubmitRecord}s and convert them to submit requirements.
*/
- Map<SubmitRequirement, SubmitRequirementResult> evaluateAllRequirements(ChangeData cd);
+ Map<SubmitRequirement, SubmitRequirementResult> evaluateAllRequirements(
+ ChangeData cd, boolean includeLegacy);
/** Evaluate a single {@link SubmitRequirement} using change data. */
SubmitRequirementResult evaluateRequirement(SubmitRequirement sr, ChangeData cd);
diff --git a/java/com/google/gerrit/server/project/SubmitRequirementsEvaluatorImpl.java b/java/com/google/gerrit/server/project/SubmitRequirementsEvaluatorImpl.java
index 9be50c7..de637b4 100644
--- a/java/com/google/gerrit/server/project/SubmitRequirementsEvaluatorImpl.java
+++ b/java/com/google/gerrit/server/project/SubmitRequirementsEvaluatorImpl.java
@@ -17,8 +17,6 @@
import static com.google.gerrit.server.project.ProjectCache.illegalState;
import com.google.common.collect.ImmutableMap;
-import com.google.gerrit.entities.LabelType;
-import com.google.gerrit.entities.SubmitRecord;
import com.google.gerrit.entities.SubmitRequirement;
import com.google.gerrit.entities.SubmitRequirementExpression;
import com.google.gerrit.entities.SubmitRequirementExpressionResult;
@@ -36,12 +34,8 @@
import com.google.inject.Provider;
import com.google.inject.Scopes;
import java.util.HashMap;
-import java.util.List;
import java.util.Map;
import java.util.Optional;
-import java.util.function.Function;
-import java.util.stream.Collectors;
-import org.eclipse.jgit.lib.ObjectId;
/** Evaluates submit requirements for different change data. */
public class SubmitRequirementsEvaluatorImpl implements SubmitRequirementsEvaluator {
@@ -81,12 +75,19 @@
}
@Override
- public Map<SubmitRequirement, SubmitRequirementResult> evaluateAllRequirements(ChangeData cd) {
- Map<SubmitRequirement, SubmitRequirementResult> result = getRequirements(cd);
- if (experimentFeatures.isFeatureEnabled(
- ExperimentFeaturesConstants
- .GERRIT_BACKEND_REQUEST_FEATURE_ENABLE_LEGACY_SUBMIT_REQUIREMENTS)) {
- result.putAll(getLegacyRequirements(cd));
+ public Map<SubmitRequirement, SubmitRequirementResult> evaluateAllRequirements(
+ ChangeData cd, boolean includeLegacy) {
+ Map<SubmitRequirement, SubmitRequirementResult> projectConfigRequirements = getRequirements(cd);
+ Map<SubmitRequirement, SubmitRequirementResult> result = projectConfigRequirements;
+ if (includeLegacy
+ && experimentFeatures.isFeatureEnabled(
+ ExperimentFeaturesConstants
+ .GERRIT_BACKEND_REQUEST_FEATURE_ENABLE_LEGACY_SUBMIT_REQUIREMENTS)) {
+ Map<SubmitRequirement, SubmitRequirementResult> legacyReqs =
+ SubmitRequirementsAdapter.getLegacyRequirements(legacyEvaluator, cd);
+ result =
+ SubmitRequirementsUtil.mergeLegacyAndNonLegacyRequirements(
+ projectConfigRequirements, legacyReqs);
}
return ImmutableMap.copyOf(result);
}
@@ -140,23 +141,6 @@
return result;
}
- /**
- * Convert and return legacy submit records (created by label functions and other {@link
- * com.google.gerrit.server.rules.SubmitRule}s to submit requirement results.
- */
- private Map<SubmitRequirement, SubmitRequirementResult> getLegacyRequirements(ChangeData cd) {
- // We use SubmitRuleOptions.defaults() which does not recompute submit rules for closed changes.
- // This doesn't have an effect since we never call this class (i.e. to evaluate submit
- // requirements) for closed changes.
- List<SubmitRecord> records = legacyEvaluator.create(SubmitRuleOptions.defaults()).evaluate(cd);
- List<LabelType> labelTypes = cd.getLabelTypes().getLabelTypes();
- ObjectId commitId = cd.currentPatchSet().commitId();
- return records.stream()
- .map(r -> SubmitRequirementsAdapter.createResult(r, labelTypes, commitId))
- .flatMap(List::stream)
- .collect(Collectors.toMap(sr -> sr.submitRequirement(), Function.identity()));
- }
-
/** Evaluate the predicate recursively using change data. */
private PredicateResult evaluatePredicateTree(
Predicate<ChangeData> predicate, ChangeData changeData) {
diff --git a/java/com/google/gerrit/server/project/SubmitRequirementsUtil.java b/java/com/google/gerrit/server/project/SubmitRequirementsUtil.java
new file mode 100644
index 0000000..2e43eac
--- /dev/null
+++ b/java/com/google/gerrit/server/project/SubmitRequirementsUtil.java
@@ -0,0 +1,71 @@
+// 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.project;
+
+import com.google.gerrit.entities.SubmitRequirement;
+import com.google.gerrit.entities.SubmitRequirementResult;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+/**
+ * A utility class for different operations related to {@link
+ * com.google.gerrit.entities.SubmitRequirement}s.
+ */
+public class SubmitRequirementsUtil {
+
+ private SubmitRequirementsUtil() {}
+
+ /**
+ * Merge legacy and non-legacy submit requirement results. If both input maps have submit
+ * requirements with the same name and fulfillment status (according to {@link
+ * SubmitRequirementResult#fulfilled()}), we eliminate the entry from the {@code
+ * legacyRequirements} input map and only include the one from the {@code
+ * projectConfigRequirements} in the result.
+ *
+ * @param projectConfigRequirements map of {@link SubmitRequirement} to {@link
+ * SubmitRequirementResult} containing results for submit requirements stored in the
+ * project.config.
+ * @param legacyRequirements map of {@link SubmitRequirement} to {@link SubmitRequirementResult}
+ * containing the results of converting legacy submit records to submit requirements.
+ * @return a map that is the result of merging both input maps, while eliminating requirements
+ * with the same name and status.
+ */
+ public static Map<SubmitRequirement, SubmitRequirementResult> mergeLegacyAndNonLegacyRequirements(
+ Map<SubmitRequirement, SubmitRequirementResult> projectConfigRequirements,
+ Map<SubmitRequirement, SubmitRequirementResult> legacyRequirements) {
+ Map<SubmitRequirement, SubmitRequirementResult> result = new HashMap<>();
+ result.putAll(projectConfigRequirements);
+ Map<String, SubmitRequirementResult> requirementsByName =
+ projectConfigRequirements.entrySet().stream()
+ .collect(Collectors.toMap(sr -> sr.getKey().name(), sr -> sr.getValue()));
+ for (Map.Entry<SubmitRequirement, SubmitRequirementResult> legacy :
+ legacyRequirements.entrySet()) {
+ String name = legacy.getKey().name();
+ SubmitRequirementResult projectConfigResult = requirementsByName.get(name);
+ SubmitRequirementResult legacyResult = legacy.getValue();
+ if (projectConfigResult != null && matchByStatus(projectConfigResult, legacyResult)) {
+ continue;
+ }
+ result.put(legacy.getKey(), legacy.getValue());
+ }
+ return result;
+ }
+
+ /** Returns true if both input results are equal in allowing/disallowing change submission. */
+ private static boolean matchByStatus(SubmitRequirementResult r1, SubmitRequirementResult r2) {
+ return r1.fulfilled() == r2.fulfilled();
+ }
+}
diff --git a/java/com/google/gerrit/server/query/approval/ListOfFilesUnchangedPredicate.java b/java/com/google/gerrit/server/query/approval/ListOfFilesUnchangedPredicate.java
index 55c27be..de7dd0a 100644
--- a/java/com/google/gerrit/server/query/approval/ListOfFilesUnchangedPredicate.java
+++ b/java/com/google/gerrit/server/query/approval/ListOfFilesUnchangedPredicate.java
@@ -58,13 +58,18 @@
Integer parentNum =
isInitialCommit(ctx.changeNotes().getProjectName(), targetPatchSet.commitId()) ? 0 : 1;
try {
- Map<String, FileDiffOutput> modifiedTargetPatchSet =
+ Map<String, FileDiffOutput> baseVsCurrent =
diffOperations.listModifiedFilesAgainstParent(
ctx.changeNotes().getProjectName(), targetPatchSet.commitId(), parentNum);
- Map<String, FileDiffOutput> modifiedSourcePatchSet =
+ Map<String, FileDiffOutput> baseVsPrior =
diffOperations.listModifiedFilesAgainstParent(
ctx.changeNotes().getProjectName(), sourcePatchSet.commitId(), parentNum);
- return match(modifiedTargetPatchSet, modifiedSourcePatchSet);
+ Map<String, FileDiffOutput> priorVsCurrent =
+ diffOperations.listModifiedFiles(
+ ctx.changeNotes().getProjectName(),
+ sourcePatchSet.commitId(),
+ targetPatchSet.commitId());
+ return match(baseVsCurrent, baseVsPrior, priorVsCurrent);
} catch (DiffNotAvailableException ex) {
throw new StorageException(
"failed to compute difference in files, so won't copy"
@@ -79,16 +84,23 @@
* {@link ChangeType} matches for each modified file.
*/
public boolean match(
- Map<String, FileDiffOutput> modifiedFiles1, Map<String, FileDiffOutput> modifiedFiles2) {
+ Map<String, FileDiffOutput> baseVsCurrent,
+ Map<String, FileDiffOutput> baseVsPrior,
+ Map<String, FileDiffOutput> priorVsCurrent) {
Set<String> allFiles = new HashSet<>();
- allFiles.addAll(modifiedFiles1.keySet());
- allFiles.addAll(modifiedFiles2.keySet());
+ allFiles.addAll(baseVsCurrent.keySet());
+ allFiles.addAll(baseVsPrior.keySet());
for (String file : allFiles) {
if (Patch.isMagic(file)) {
continue;
}
- FileDiffOutput fileDiffOutput1 = modifiedFiles1.get(file);
- FileDiffOutput fileDiffOutput2 = modifiedFiles2.get(file);
+ FileDiffOutput fileDiffOutput1 = baseVsCurrent.get(file);
+ FileDiffOutput fileDiffOutput2 = baseVsPrior.get(file);
+ if (!priorVsCurrent.containsKey(file)) {
+ // If the file is not modified between prior and current patchsets, then scan safely skip
+ // it. The file might has been modified due to rebase.
+ continue;
+ }
if (fileDiffOutput1 == null || fileDiffOutput2 == null) {
return false;
}
diff --git a/java/com/google/gerrit/server/query/change/ChangeData.java b/java/com/google/gerrit/server/query/change/ChangeData.java
index 6c74301..b8c8c07 100644
--- a/java/com/google/gerrit/server/query/change/ChangeData.java
+++ b/java/com/google/gerrit/server/query/change/ChangeData.java
@@ -75,6 +75,8 @@
import com.google.gerrit.server.change.PureRevert;
import com.google.gerrit.server.config.AllUsersName;
import com.google.gerrit.server.config.TrackingFooters;
+import com.google.gerrit.server.experiments.ExperimentFeatures;
+import com.google.gerrit.server.experiments.ExperimentFeaturesConstants;
import com.google.gerrit.server.git.GitRepositoryManager;
import com.google.gerrit.server.git.MergeUtil;
import com.google.gerrit.server.notedb.ChangeNotes;
@@ -87,7 +89,9 @@
import com.google.gerrit.server.project.NoSuchChangeException;
import com.google.gerrit.server.project.ProjectCache;
import com.google.gerrit.server.project.ProjectState;
+import com.google.gerrit.server.project.SubmitRequirementsAdapter;
import com.google.gerrit.server.project.SubmitRequirementsEvaluator;
+import com.google.gerrit.server.project.SubmitRequirementsUtil;
import com.google.gerrit.server.project.SubmitRuleEvaluator;
import com.google.gerrit.server.project.SubmitRuleOptions;
import com.google.gerrit.server.util.time.TimeUtil;
@@ -268,7 +272,7 @@
ChangeData cd =
new ChangeData(
null, null, null, null, null, null, null, null, null, null, null, null, null, null,
- null, null, project, id, null, null);
+ null, null, null, project, id, null, null);
cd.currentPatchSet =
PatchSet.builder()
.id(PatchSet.id(id, currentPatchSetId))
@@ -286,6 +290,7 @@
private final ChangeMessagesUtil cmUtil;
private final ChangeNotes.Factory notesFactory;
private final CommentsUtil commentsUtil;
+ private final ExperimentFeatures experimentFeatures;
private final GitRepositoryManager repoManager;
private final MergeUtil.Factory mergeUtilFactory;
private final MergeabilityCache mergeabilityCache;
@@ -367,6 +372,7 @@
ChangeMessagesUtil cmUtil,
ChangeNotes.Factory notesFactory,
CommentsUtil commentsUtil,
+ ExperimentFeatures experimentFeatures,
GitRepositoryManager repoManager,
MergeUtil.Factory mergeUtilFactory,
MergeabilityCache mergeabilityCache,
@@ -386,6 +392,7 @@
this.cmUtil = cmUtil;
this.notesFactory = notesFactory;
this.commentsUtil = commentsUtil;
+ this.experimentFeatures = experimentFeatures;
this.repoManager = repoManager;
this.mergeUtilFactory = mergeUtilFactory;
this.mergeabilityCache = mergeabilityCache;
@@ -946,13 +953,28 @@
return Collections.emptyMap();
}
Change c = change();
- if (c != null && c.isClosed()) {
+ if (c == null || !c.isClosed()) {
+ // Open changes: Evaluate submit requirements online.
submitRequirements =
- notes().getSubmitRequirementsResult().stream()
- .collect(Collectors.toMap(r -> r.submitRequirement(), Function.identity()));
- } else {
- submitRequirements = submitRequirementsEvaluator.evaluateAllRequirements(this);
+ submitRequirementsEvaluator.evaluateAllRequirements(this, /* includeLegacy= */ true);
+ return submitRequirements;
}
+ // Closed changes: Load submit requirement results from NoteDb.
+ Map<SubmitRequirement, SubmitRequirementResult> projectConfigRequirements =
+ notes().getSubmitRequirementsResult().stream()
+ .filter(r -> !r.legacy())
+ .collect(Collectors.toMap(r -> r.submitRequirement(), Function.identity()));
+ if (!experimentFeatures.isFeatureEnabled(
+ ExperimentFeaturesConstants
+ .GERRIT_BACKEND_REQUEST_FEATURE_ENABLE_LEGACY_SUBMIT_REQUIREMENTS)) {
+ submitRequirements = projectConfigRequirements;
+ return submitRequirements;
+ }
+ Map<SubmitRequirement, SubmitRequirementResult> legacyRequirements =
+ SubmitRequirementsAdapter.getLegacyRequirements(submitRuleEvaluatorFactory, this);
+ submitRequirements =
+ SubmitRequirementsUtil.mergeLegacyAndNonLegacyRequirements(
+ projectConfigRequirements, legacyRequirements);
}
return submitRequirements;
}
diff --git a/java/com/google/gerrit/server/restapi/change/GetRelated.java b/java/com/google/gerrit/server/restapi/change/GetRelated.java
index ba1a1dc..0eef468 100644
--- a/java/com/google/gerrit/server/restapi/change/GetRelated.java
+++ b/java/com/google/gerrit/server/restapi/change/GetRelated.java
@@ -14,10 +14,6 @@
package com.google.gerrit.server.restapi.change;
-import static com.google.common.base.Preconditions.checkArgument;
-import static java.util.stream.Collectors.toSet;
-
-import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.Lists;
import com.google.common.flogger.FluentLogger;
import com.google.gerrit.common.Nullable;
@@ -29,56 +25,39 @@
import com.google.gerrit.extensions.common.CommitInfo;
import com.google.gerrit.extensions.restapi.Response;
import com.google.gerrit.extensions.restapi.RestReadView;
-import com.google.gerrit.index.IndexConfig;
import com.google.gerrit.server.ChangeUtil;
import com.google.gerrit.server.CommonConverters;
-import com.google.gerrit.server.PatchSetUtil;
+import com.google.gerrit.server.change.GetRelatedChangesUtil;
+import com.google.gerrit.server.change.RelatedChangesSorter;
import com.google.gerrit.server.change.RevisionResource;
-import com.google.gerrit.server.notedb.ChangeNotes;
import com.google.gerrit.server.permissions.PermissionBackendException;
import com.google.gerrit.server.project.NoSuchProjectException;
import com.google.gerrit.server.query.change.ChangeData;
-import com.google.gerrit.server.query.change.InternalChangeQuery;
import com.google.inject.Inject;
-import com.google.inject.Provider;
import com.google.inject.Singleton;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Locale;
-import java.util.Set;
-import org.eclipse.jgit.errors.RepositoryNotFoundException;
import org.eclipse.jgit.revwalk.RevCommit;
@Singleton
public class GetRelated implements RestReadView<RevisionResource> {
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
- private final Provider<InternalChangeQuery> queryProvider;
- private final PatchSetUtil psUtil;
- private final RelatedChangesSorter sorter;
- private final IndexConfig indexConfig;
private final ChangeData.Factory changeDataFactory;
+ private final GetRelatedChangesUtil getRelatedChangesUtil;
@Inject
- GetRelated(
- Provider<InternalChangeQuery> queryProvider,
- PatchSetUtil psUtil,
- RelatedChangesSorter sorter,
- IndexConfig indexConfig,
- ChangeData.Factory changeDataFactory) {
- this.queryProvider = queryProvider;
- this.psUtil = psUtil;
- this.sorter = sorter;
- this.indexConfig = indexConfig;
+ GetRelated(ChangeData.Factory changeDataFactory, GetRelatedChangesUtil getRelatedChangesUtil) {
this.changeDataFactory = changeDataFactory;
+ this.getRelatedChangesUtil = getRelatedChangesUtil;
}
@Override
public Response<RelatedChangesInfo> apply(RevisionResource rsrc)
- throws RepositoryNotFoundException, IOException, NoSuchProjectException,
- PermissionBackendException {
+ throws IOException, NoSuchProjectException, PermissionBackendException {
RelatedChangesInfo relatedChangesInfo = new RelatedChangesInfo();
relatedChangesInfo.changes = getRelated(rsrc);
return Response.ok(relatedChangesInfo);
@@ -86,30 +65,15 @@
public List<RelatedChangeAndCommitInfo> getRelated(RevisionResource rsrc)
throws IOException, PermissionBackendException {
- Set<String> groups = getAllGroups(rsrc.getNotes(), psUtil);
- logger.atFine().log("groups = %s", groups);
- if (groups.isEmpty()) {
- return Collections.emptyList();
- }
-
- List<ChangeData> cds =
- InternalChangeQuery.byProjectGroups(
- queryProvider, indexConfig, rsrc.getChange().getProject(), groups);
- if (cds.isEmpty()) {
- return Collections.emptyList();
- }
- if (cds.size() == 1 && cds.get(0).getId().equals(rsrc.getChange().getId())) {
- return Collections.emptyList();
- }
- List<RelatedChangeAndCommitInfo> result = new ArrayList<>(cds.size());
-
boolean isEdit = rsrc.getEdit().isPresent();
PatchSet basePs = isEdit ? rsrc.getEdit().get().getBasePatchSet() : rsrc.getPatchSet();
logger.atFine().log("isEdit = %s, basePs = %s", isEdit, basePs);
- cds = reloadChangeIfStale(cds, rsrc.getChange(), basePs);
+ List<RelatedChangesSorter.PatchSetData> sortedResult =
+ getRelatedChangesUtil.getRelated(changeDataFactory.create(rsrc.getNotes()), basePs);
- for (RelatedChangesSorter.PatchSetData d : sorter.sort(cds, basePs)) {
+ List<RelatedChangeAndCommitInfo> result = new ArrayList<>(sortedResult.size());
+ for (RelatedChangesSorter.PatchSetData d : sortedResult) {
PatchSet ps = d.patchSet();
RevCommit commit;
if (isEdit && ps.id().equals(basePs.id())) {
@@ -134,37 +98,6 @@
return result;
}
- @VisibleForTesting
- public static Set<String> getAllGroups(ChangeNotes notes, PatchSetUtil psUtil) {
- return psUtil.byChange(notes).stream().flatMap(ps -> ps.groups().stream()).collect(toSet());
- }
-
- private List<ChangeData> reloadChangeIfStale(
- List<ChangeData> changeDatasFromIndex, Change wantedChange, PatchSet wantedPs) {
- checkArgument(
- wantedChange.getId().equals(wantedPs.id().changeId()),
- "change of wantedPs (%s) doesn't match wantedChange (%s)",
- wantedPs.id().changeId(),
- wantedChange.getId());
-
- List<ChangeData> changeDatas = new ArrayList<>(changeDatasFromIndex.size() + 1);
- changeDatas.addAll(changeDatasFromIndex);
-
- // Reload the change in case the patch set is absent.
- changeDatas.stream()
- .filter(
- cd -> cd.getId().equals(wantedPs.id().changeId()) && cd.patchSet(wantedPs.id()) == null)
- .forEach(ChangeData::reloadChange);
-
- if (changeDatas.stream().noneMatch(cd -> cd.getId().equals(wantedPs.id().changeId()))) {
- // The change of the wanted patch set is missing in the result from the index.
- // Load it from NoteDb and add it to the result.
- changeDatas.add(changeDataFactory.create(wantedChange));
- }
-
- return changeDatas;
- }
-
static RelatedChangeAndCommitInfo newChangeAndCommit(
Project.NameKey project, @Nullable Change change, @Nullable PatchSet ps, RevCommit c) {
RelatedChangeAndCommitInfo info = new RelatedChangeAndCommitInfo();
diff --git a/java/com/google/gerrit/server/restapi/change/PostReview.java b/java/com/google/gerrit/server/restapi/change/PostReview.java
index 5002a82..5c252f4 100644
--- a/java/com/google/gerrit/server/restapi/change/PostReview.java
+++ b/java/com/google/gerrit/server/restapi/change/PostReview.java
@@ -92,7 +92,9 @@
import com.google.gerrit.server.PatchSetUtil;
import com.google.gerrit.server.PublishCommentUtil;
import com.google.gerrit.server.ReviewerSet;
+import com.google.gerrit.server.account.AccountCache;
import com.google.gerrit.server.account.AccountResolver;
+import com.google.gerrit.server.account.AccountState;
import com.google.gerrit.server.approval.ApprovalsUtil;
import com.google.gerrit.server.change.ChangeResource;
import com.google.gerrit.server.change.EmailReviewComments;
@@ -105,6 +107,7 @@
import com.google.gerrit.server.change.WorkInProgressOp;
import com.google.gerrit.server.config.GerritServerConfig;
import com.google.gerrit.server.extensions.events.CommentAdded;
+import com.google.gerrit.server.extensions.events.ReviewerAdded;
import com.google.gerrit.server.logging.Metadata;
import com.google.gerrit.server.logging.TraceContext;
import com.google.gerrit.server.notedb.ChangeNotes;
@@ -187,6 +190,7 @@
private final BatchUpdate.Factory updateFactory;
private final ChangeResource.Factory changeResourceFactory;
private final ChangeData.Factory changeDataFactory;
+ private final AccountCache accountCache;
private final ApprovalsUtil approvalsUtil;
private final ChangeMessagesUtil cmUtil;
private final CommentsUtil commentsUtil;
@@ -206,6 +210,7 @@
private final PluginSetContext<CommentValidator> commentValidators;
private final PluginSetContext<OnPostReview> onPostReviews;
private final ReplyAttentionSetUpdates replyAttentionSetUpdates;
+ private final ReviewerAdded reviewerAdded;
private final boolean strictLabels;
private final boolean publishPatchSetLevelComment;
@@ -214,6 +219,7 @@
BatchUpdate.Factory updateFactory,
ChangeResource.Factory changeResourceFactory,
ChangeData.Factory changeDataFactory,
+ AccountCache accountCache,
ApprovalsUtil approvalsUtil,
ChangeMessagesUtil cmUtil,
CommentsUtil commentsUtil,
@@ -233,10 +239,12 @@
PermissionBackend permissionBackend,
PluginSetContext<CommentValidator> commentValidators,
PluginSetContext<OnPostReview> onPostReviews,
- ReplyAttentionSetUpdates replyAttentionSetUpdates) {
+ ReplyAttentionSetUpdates replyAttentionSetUpdates,
+ ReviewerAdded reviewerAdded) {
this.updateFactory = updateFactory;
this.changeResourceFactory = changeResourceFactory;
this.changeDataFactory = changeDataFactory;
+ this.accountCache = accountCache;
this.commentsUtil = commentsUtil;
this.publishCommentUtil = publishCommentUtil;
this.psUtil = psUtil;
@@ -256,6 +264,7 @@
this.commentValidators = commentValidators;
this.onPostReviews = onPostReviews;
this.replyAttentionSetUpdates = replyAttentionSetUpdates;
+ this.reviewerAdded = reviewerAdded;
this.strictLabels = gerritConfig.getBoolean("change", "strictLabels", false);
this.publishPatchSetLevelComment =
gerritConfig.getBoolean("event", "comment-added", "publishPatchSetLevelComment", true);
@@ -371,6 +380,7 @@
logger.atFine().log("adding reviewer additions");
for (ReviewerModification reviewerResult : reviewerResults) {
reviewerResult.op.suppressEmail(); // Send a single batch email below.
+ reviewerResult.op.suppressEvent(); // Send events below, if possible as batch.
bu.addOp(revision.getChange().getId(), reviewerResult.op);
if (!ccOrReviewer && reviewerResult.reviewers.contains(account)) {
logger.atFine().log("calling user is explicitly added as reviewer or CC");
@@ -386,6 +396,7 @@
ReviewerModification selfAddition =
reviewerModifier.ccCurrentUser(revision.getUser(), revision);
selfAddition.op.suppressEmail();
+ selfAddition.op.suppressEvent();
bu.addOp(revision.getChange().getId(), selfAddition.op);
}
@@ -433,8 +444,10 @@
reviewerResult.gatherResults(cd);
}
- // Sending from AddReviewersOp was suppressed so we can send a single batch email here.
+ // Sending emails and events from ReviewersOps was suppressed so we can send a single batch
+ // email/event here.
batchEmailReviewers(revision.getUser(), revision.getChange(), reviewerResults, notify);
+ batchReviewerEvents(revision.getUser(), cd, revision.getPatchSet(), reviewerResults, ts);
}
return Response.ok(output);
@@ -512,6 +525,35 @@
}
}
+ private void batchReviewerEvents(
+ CurrentUser user,
+ ChangeData cd,
+ PatchSet patchSet,
+ List<ReviewerModification> reviewerModifications,
+ Timestamp when) {
+ List<AccountState> newlyAddedReviewers = new ArrayList<>();
+
+ // There are no events for CCs and reviewers added/deleted by email.
+ for (ReviewerModification modification : reviewerModifications) {
+ Result reviewerAdditionResult = modification.op.getResult();
+ if (modification.state() == ReviewerState.REVIEWER) {
+ newlyAddedReviewers.addAll(
+ reviewerAdditionResult.addedReviewers().stream()
+ .map(psa -> psa.accountId())
+ .map(accountId -> accountCache.get(accountId))
+ .flatMap(Streams::stream)
+ .collect(toList()));
+ } else if (modification.state() == ReviewerState.REMOVED) {
+ // There is no batch event for reviewer removals, hence fire the event for each
+ // modification that deleted a reviewer immediately.
+ modification.op.sendEvent();
+ }
+ }
+
+ // Fire a batch event for all newly added reviewers.
+ reviewerAdded.fire(cd, patchSet, newlyAddedReviewers, user.asIdentifiedUser().state(), when);
+ }
+
private RevisionResource onBehalfOf(RevisionResource rev, LabelTypes labelTypes, ReviewInput in)
throws BadRequestException, AuthException, UnprocessableEntityException,
PermissionBackendException, IOException, ConfigInvalidException {
diff --git a/java/com/google/gerrit/server/restapi/project/ListProjects.java b/java/com/google/gerrit/server/restapi/project/ListProjects.java
index c4ae33a..4d8005b 100644
--- a/java/com/google/gerrit/server/restapi/project/ListProjects.java
+++ b/java/com/google/gerrit/server/restapi/project/ListProjects.java
@@ -490,7 +490,7 @@
continue;
}
- List<Ref> refs = retrieveBranchRefs(e);
+ List<Ref> refs = retrieveBranchRefs(e, git);
if (!hasValidRef(refs)) {
continue;
}
@@ -578,17 +578,12 @@
}
}
- private List<Ref> retrieveBranchRefs(ProjectState e) throws PermissionBackendException {
- boolean canReadAllRefs = e.statePermitsRead();
- if (canReadAllRefs) {
- try {
- permissionBackend.user(currentUser).project(e.getNameKey()).check(ProjectPermission.READ);
- } catch (AuthException exp) {
- canReadAllRefs = false;
- }
+ private List<Ref> retrieveBranchRefs(ProjectState e, Repository git) {
+ if (!e.statePermitsRead()) {
+ return ImmutableList.of();
}
- return getBranchRefs(e.getNameKey(), canReadAllRefs);
+ return getBranchRefs(e.getNameKey(), git);
}
private void addParentProjectInfo(
@@ -708,15 +703,13 @@
stdout.flush();
}
- private List<Ref> getBranchRefs(Project.NameKey projectName, boolean canReadAllRefs) {
+ private List<Ref> getBranchRefs(Project.NameKey projectName, Repository git) {
Ref[] result = new Ref[showBranch.size()];
- try (Repository git = repoManager.openRepository(projectName)) {
+ try {
PermissionBackend.ForProject perm = permissionBackend.user(currentUser).project(projectName);
for (int i = 0; i < showBranch.size(); i++) {
Ref ref = git.findRef(showBranch.get(i));
- if (all && canReadAllRefs) {
- result[i] = ref;
- } else if (ref != null && ref.getObjectId() != null) {
+ if (ref != null && ref.getObjectId() != null) {
try {
perm.ref(ref.getLeaf().getName()).check(RefPermission.READ);
result[i] = ref;
diff --git a/javatests/com/google/gerrit/acceptance/api/change/ChangeIT.java b/javatests/com/google/gerrit/acceptance/api/change/ChangeIT.java
index ac67444..52202d7 100644
--- a/javatests/com/google/gerrit/acceptance/api/change/ChangeIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/change/ChangeIT.java
@@ -99,10 +99,12 @@
import com.google.gerrit.entities.LabelFunction;
import com.google.gerrit.entities.LabelId;
import com.google.gerrit.entities.LabelType;
+import com.google.gerrit.entities.LegacySubmitRequirement;
import com.google.gerrit.entities.PatchSet;
import com.google.gerrit.entities.Permission;
import com.google.gerrit.entities.Project;
import com.google.gerrit.entities.RefNames;
+import com.google.gerrit.entities.SubmitRecord;
import com.google.gerrit.entities.SubmitRequirement;
import com.google.gerrit.entities.SubmitRequirementExpression;
import com.google.gerrit.entities.SubmitRequirementExpressionResult;
@@ -148,7 +150,9 @@
import com.google.gerrit.extensions.common.CommitInfo;
import com.google.gerrit.extensions.common.GitPerson;
import com.google.gerrit.extensions.common.LabelInfo;
+import com.google.gerrit.extensions.common.LegacySubmitRequirementInfo;
import com.google.gerrit.extensions.common.RevisionInfo;
+import com.google.gerrit.extensions.common.SubmitRecordInfo;
import com.google.gerrit.extensions.common.SubmitRequirementResultInfo;
import com.google.gerrit.extensions.common.SubmitRequirementResultInfo.Status;
import com.google.gerrit.extensions.common.TrackingIdInfo;
@@ -180,12 +184,11 @@
import com.google.gerrit.server.patch.DiffSummaryKey;
import com.google.gerrit.server.patch.IntraLineDiff;
import com.google.gerrit.server.patch.IntraLineDiffKey;
-import com.google.gerrit.server.patch.PatchList;
-import com.google.gerrit.server.patch.PatchListKey;
import com.google.gerrit.server.project.testing.TestLabels;
import com.google.gerrit.server.query.change.ChangeData;
import com.google.gerrit.server.query.change.ChangeQueryBuilder.ChangeOperatorFactory;
import com.google.gerrit.server.restapi.change.PostReview;
+import com.google.gerrit.server.rules.SubmitRule;
import com.google.gerrit.server.update.BatchUpdate;
import com.google.gerrit.server.update.BatchUpdateOp;
import com.google.gerrit.server.update.ChangeContext;
@@ -238,10 +241,6 @@
@Inject private ExtensionRegistry extensionRegistry;
@Inject
- @Named("diff")
- private Cache<PatchListKey, PatchList> fileCache;
-
- @Inject
@Named("diff_intraline")
private Cache<IntraLineDiffKey, IntraLineDiff> intraCache;
@@ -302,13 +301,10 @@
String fileContent = "First line\nSecond line\n";
PushOneCommit.Result result = createChange("Add a file", fileName, fileContent);
String triplet = project.get() + "~master~" + result.getChangeId();
- CacheStats startPatch = cloneStats(fileCache.stats());
CacheStats startIntra = cloneStats(intraCache.stats());
CacheStats startSummary = cloneStats(diffSummaryCache.stats());
gApi.changes().id(triplet).get(ImmutableList.of(ListChangesOption.SKIP_DIFFSTAT));
- assertThat(fileCache.stats()).since(startPatch).hasMissCount(0);
- assertThat(fileCache.stats()).since(startPatch).hasHitCount(0);
assertThat(intraCache.stats()).since(startIntra).hasMissCount(0);
assertThat(intraCache.stats()).since(startIntra).hasHitCount(0);
assertThat(diffSummaryCache.stats()).since(startSummary).hasMissCount(0);
@@ -4033,6 +4029,51 @@
}
@Test
+ public void submitRecords() throws Exception {
+ PushOneCommit.Result r = createChange();
+ TestSubmitRule testSubmitRule = new TestSubmitRule();
+ try (Registration registration = extensionRegistry.newRegistration().add(testSubmitRule)) {
+ String changeId = r.getChangeId();
+
+ ChangeInfo change = gApi.changes().id(changeId).get();
+ assertThat(change.submitRecords).hasSize(2);
+ // Check the default submit record for the code-review label
+ SubmitRecordInfo codeReviewRecord = Iterables.get(change.submitRecords, 0);
+ assertThat(codeReviewRecord.ruleName).isEqualTo("gerrit~DefaultSubmitRule");
+ assertThat(codeReviewRecord.status).isEqualTo(SubmitRecordInfo.Status.NOT_READY);
+ assertThat(codeReviewRecord.labels).hasSize(1);
+ SubmitRecordInfo.Label label = Iterables.getOnlyElement(codeReviewRecord.labels);
+ assertThat(label.label).isEqualTo("Code-Review");
+ assertThat(label.status).isEqualTo(SubmitRecordInfo.Label.Status.NEED);
+ assertThat(label.appliedBy).isNull();
+ // Check the custom test record created by the TestSubmitRule
+ SubmitRecordInfo testRecord = Iterables.get(change.submitRecords, 1);
+ assertThat(testRecord.ruleName).isEqualTo("gerrit~TestSubmitRule");
+ assertThat(testRecord.status).isEqualTo(SubmitRecordInfo.Status.OK);
+ assertThat(testRecord.requirements)
+ .containsExactly(new LegacySubmitRequirementInfo("OK", "fallback text", "type"));
+ assertThat(testRecord.labels).hasSize(1);
+ SubmitRecordInfo.Label testLabel = Iterables.getOnlyElement(testRecord.labels);
+ assertThat(testLabel.label).isEqualTo("label");
+ assertThat(testLabel.status).isEqualTo(SubmitRecordInfo.Label.Status.OK);
+ assertThat(testLabel.appliedBy).isNull();
+
+ voteLabel(changeId, "code-review", 2);
+ // Code review record is satisfied after voting +2
+ change = gApi.changes().id(changeId).get();
+ assertThat(change.submitRecords).hasSize(2);
+ codeReviewRecord = Iterables.get(change.submitRecords, 0);
+ assertThat(codeReviewRecord.ruleName).isEqualTo("gerrit~DefaultSubmitRule");
+ assertThat(codeReviewRecord.status).isEqualTo(SubmitRecordInfo.Status.OK);
+ assertThat(codeReviewRecord.labels).hasSize(1);
+ label = Iterables.getOnlyElement(codeReviewRecord.labels);
+ assertThat(label.label).isEqualTo("Code-Review");
+ assertThat(label.status).isEqualTo(SubmitRecordInfo.Label.Status.OK);
+ assertThat(label.appliedBy._accountId).isEqualTo(admin.id().get());
+ }
+ }
+
+ @Test
public void submitRequirement_withLabelEqualsMax() throws Exception {
configSubmitRequirement(
project,
@@ -4498,6 +4539,129 @@
value =
ExperimentFeaturesConstants
.GERRIT_BACKEND_REQUEST_FEATURE_ENABLE_LEGACY_SUBMIT_REQUIREMENTS)
+ public void
+ submitRequirements_returnOneEntryForMatchingLegacyAndNonLegacyResultsWithTheSameName_ifLegacySubmitRecordsAreEnabled()
+ throws Exception {
+ // Configure a legacy submit requirement: label with a max with block function
+ configLabel("build-cop-override", LabelFunction.MAX_WITH_BLOCK);
+ projectOperations
+ .project(project)
+ .forUpdate()
+ .add(
+ allowLabel("build-cop-override")
+ .ref("refs/heads/master")
+ .group(REGISTERED_USERS)
+ .range(-1, 1))
+ .update();
+
+ // Configure a submit requirement with the same name.
+ configSubmitRequirement(
+ project,
+ SubmitRequirement.builder()
+ .setName("build-cop-override")
+ .setSubmittabilityExpression(
+ SubmitRequirementExpression.create(
+ "label:build-cop-override=MAX -label:build-cop-override=MIN"))
+ .setAllowOverrideInChildProjects(false)
+ .build());
+
+ // Create a change. Vote to fulfill all requirements.
+ PushOneCommit.Result r = createChange();
+ String changeId = r.getChangeId();
+ voteLabel(changeId, "build-cop-override", 1);
+ voteLabel(changeId, "Code-Review", 2);
+
+ // Project has two legacy requirements: Code-Review and bco, and a non-legacy requirement: bco.
+ // Only non-legacy bco is returned.
+ ChangeInfo change = gApi.changes().id(changeId).get();
+ assertThat(change.submitRequirements).hasSize(2);
+ assertSubmitRequirementStatus(
+ change.submitRequirements, "Code-Review", Status.SATISFIED, /* isLegacy= */ true);
+ assertSubmitRequirementStatus(
+ change.submitRequirements,
+ "build-cop-override",
+ Status.SATISFIED,
+ /* isLegacy= */ false,
+ /* submittabilityCondition= */ "label:build-cop-override=MAX -label:build-cop-override=MIN");
+
+ // Merge the change. Submit requirements are still the same.
+ gApi.changes().id(changeId).current().submit();
+ change = gApi.changes().id(changeId).get();
+ assertThat(change.submitRequirements).hasSize(2);
+ assertSubmitRequirementStatus(
+ change.submitRequirements, "Code-Review", Status.SATISFIED, /* isLegacy= */ true);
+ assertSubmitRequirementStatus(
+ change.submitRequirements,
+ "build-cop-override",
+ Status.SATISFIED,
+ /* isLegacy= */ false,
+ /* submittabilityCondition= */ "label:build-cop-override=MAX -label:build-cop-override=MIN");
+ }
+
+ @Test
+ @GerritConfig(
+ name = "experiments.enabled",
+ value =
+ ExperimentFeaturesConstants
+ .GERRIT_BACKEND_REQUEST_FEATURE_ENABLE_LEGACY_SUBMIT_REQUIREMENTS)
+ public void
+ submitRequirements_returnTwoEntriesForMismatchingLegacyAndNonLegacyResultsWithTheSameName_ifLegacySubmitRecordsAreEnabled()
+ throws Exception {
+ // Configure a legacy submit requirement: label with a max with block function
+ configLabel("build-cop-override", LabelFunction.MAX_WITH_BLOCK);
+ projectOperations
+ .project(project)
+ .forUpdate()
+ .add(
+ allowLabel("build-cop-override")
+ .ref("refs/heads/master")
+ .group(REGISTERED_USERS)
+ .range(-1, 1))
+ .update();
+
+ // Configure a submit requirement with the same name.
+ configSubmitRequirement(
+ project,
+ SubmitRequirement.builder()
+ .setName("build-cop-override")
+ .setSubmittabilityExpression(
+ SubmitRequirementExpression.create("label:build-cop-override=MIN"))
+ .setAllowOverrideInChildProjects(false)
+ .build());
+
+ // Create a change
+ PushOneCommit.Result r = createChange();
+ String changeId = r.getChangeId();
+ voteLabel(changeId, "build-cop-override", 1);
+ voteLabel(changeId, "Code-Review", 2);
+
+ // Project has two legacy requirements: Code-Review and bco, and a non-legacy requirement: bco.
+ // Two instances of bco will be returned since their status is not matching.
+ ChangeInfo change = gApi.changes().id(changeId).get();
+ assertThat(change.submitRequirements).hasSize(3);
+ assertSubmitRequirementStatus(
+ change.submitRequirements, "Code-Review", Status.SATISFIED, /* isLegacy= */ true);
+ assertSubmitRequirementStatus(
+ change.submitRequirements,
+ "build-cop-override",
+ Status.SATISFIED,
+ /* isLegacy= */ true,
+ // MAX_WITH_BLOCK function was translated to a submittability expression.
+ /* submittabilityCondition= */ "label:build-cop-override=MAX -label:build-cop-override=MIN");
+ assertSubmitRequirementStatus(
+ change.submitRequirements,
+ "build-cop-override",
+ Status.UNSATISFIED,
+ /* isLegacy= */ false,
+ /* submittabilityCondition= */ "label:build-cop-override=MIN");
+ }
+
+ @Test
+ @GerritConfig(
+ name = "experiments.enabled",
+ value =
+ ExperimentFeaturesConstants
+ .GERRIT_BACKEND_REQUEST_FEATURE_ENABLE_LEGACY_SUBMIT_REQUIREMENTS)
public void submitRequirements_returnForLegacySubmitRecords_ifEnabled() throws Exception {
configLabel("build-cop-override", LabelFunction.MAX_WITH_BLOCK);
projectOperations
@@ -4541,6 +4705,7 @@
// 4. Merge the change. Submit requirements status is presented from NoteDb.
gApi.changes().id(changeId).current().submit();
change = gApi.changes().id(changeId).get();
+ // Legacy submit records are returned as submit requirements.
assertThat(change.submitRequirements).hasSize(2);
assertSubmitRequirementStatus(
change.submitRequirements, "Code-Review", Status.SATISFIED, /* isLegacy= */ true);
@@ -4928,6 +5093,28 @@
}
@Test
+ @GerritConfig(name = "trackingid.jira-bug.footer", value = "Bug:")
+ @GerritConfig(name = "trackingid.jira-bug.match", value = "\\d+")
+ @GerritConfig(name = "trackingid.jira-bug.system", value = "JIRA")
+ public void multipleTrackingIdsInSingleFooter() throws Exception {
+ PushOneCommit push =
+ pushFactory.create(
+ admin.newIdent(),
+ testRepo,
+ PushOneCommit.SUBJECT + "\n\n" + "Bug: 123, 456",
+ PushOneCommit.FILE_NAME,
+ PushOneCommit.FILE_CONTENT);
+ PushOneCommit.Result result = push.to("refs/for/master");
+ result.assertOkStatus();
+
+ ChangeInfo change = gApi.changes().id(result.getChangeId()).get(TRACKING_IDS);
+ Collection<TrackingIdInfo> trackingIds = change.trackingIds;
+ assertThat(trackingIds).isNotNull();
+ assertThat(trackingIds).hasSize(2);
+ assertThat(trackingIds.stream().map(t -> t.id)).containsExactly("123", "456");
+ }
+
+ @Test
public void starUnstar() throws Exception {
ChangeIndexedCounter changeIndexedCounter = new ChangeIndexedCounter();
try (Registration registration =
@@ -5133,6 +5320,30 @@
Collection<SubmitRequirementResultInfo> results,
String requirementName,
SubmitRequirementResultInfo.Status status,
+ boolean isLegacy,
+ String submittabilityCondition) {
+ for (SubmitRequirementResultInfo result : results) {
+ if (result.name.equals(requirementName)
+ && result.status == status
+ && result.isLegacy == isLegacy
+ && result.submittabilityExpressionResult.expression.equals(submittabilityCondition)) {
+ return;
+ }
+ }
+ throw new AssertionError(
+ String.format(
+ "Could not find submit requirement %s with status %s (results = %s)",
+ requirementName,
+ status,
+ results.stream()
+ .map(r -> String.format("%s=%s", r.name, r.status))
+ .collect(toImmutableList())));
+ }
+
+ private void assertSubmitRequirementStatus(
+ Collection<SubmitRequirementResultInfo> results,
+ String requirementName,
+ SubmitRequirementResultInfo.Status status,
boolean isLegacy) {
for (SubmitRequirementResultInfo result : results) {
if (result.name.equals(requirementName)
@@ -5161,4 +5372,25 @@
.update();
return project;
}
+
+ /** Returns a hard-coded submit record containing all fields. */
+ private static class TestSubmitRule implements SubmitRule {
+ @Override
+ public Optional<SubmitRecord> evaluate(ChangeData changeData) {
+ SubmitRecord record = new SubmitRecord();
+ record.ruleName = "testSubmitRule";
+ record.status = SubmitRecord.Status.OK;
+ SubmitRecord.Label label = new SubmitRecord.Label();
+ label.label = "label";
+ label.status = SubmitRecord.Label.Status.OK;
+ record.labels = Arrays.asList(label);
+ record.requirements =
+ Arrays.asList(
+ LegacySubmitRequirement.builder()
+ .setType("type")
+ .setFallbackText("fallback text")
+ .build());
+ return Optional.of(record);
+ }
+ }
}
diff --git a/javatests/com/google/gerrit/acceptance/api/change/PostReviewIT.java b/javatests/com/google/gerrit/acceptance/api/change/PostReviewIT.java
index b79be80..96bc65d 100644
--- a/javatests/com/google/gerrit/acceptance/api/change/PostReviewIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/change/PostReviewIT.java
@@ -27,6 +27,7 @@
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
import com.google.common.truth.Correspondence;
import com.google.gerrit.acceptance.AbstractDaemonTest;
@@ -56,6 +57,8 @@
import com.google.gerrit.extensions.common.RobotCommentInfo;
import com.google.gerrit.extensions.config.FactoryModule;
import com.google.gerrit.extensions.events.CommentAddedListener;
+import com.google.gerrit.extensions.events.ReviewerAddedListener;
+import com.google.gerrit.extensions.events.ReviewerDeletedListener;
import com.google.gerrit.extensions.restapi.BadRequestException;
import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
import com.google.gerrit.extensions.restapi.RestApiException;
@@ -74,6 +77,7 @@
import com.google.inject.Inject;
import com.google.inject.Module;
import java.sql.Timestamp;
+import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Map;
@@ -699,6 +703,61 @@
}
@Test
+ public void addingReviewers() throws Exception {
+ PushOneCommit.Result r = createChange();
+
+ TestAccount user2 = accountCreator.user2();
+
+ TestReviewerAddedListener testReviewerAddedListener = new TestReviewerAddedListener();
+ try (Registration registration =
+ extensionRegistry.newRegistration().add(testReviewerAddedListener)) {
+ // add user and user2
+ ReviewResult reviewResult =
+ gApi.changes()
+ .id(r.getChangeId())
+ .current()
+ .review(ReviewInput.create().reviewer(user.email()).reviewer(user2.email()));
+
+ assertThat(
+ reviewResult.reviewers.values().stream()
+ .filter(a -> a.reviewers != null)
+ .map(a -> Iterables.getOnlyElement(a.reviewers).name)
+ .collect(toImmutableSet()))
+ .containsExactly(user.fullName(), user2.fullName());
+ }
+
+ assertThat(
+ gApi.changes().id(r.getChangeId()).reviewers().stream()
+ .map(a -> a.name)
+ .collect(toImmutableSet()))
+ .containsExactly(user.fullName(), user2.fullName());
+
+ // Ensure only one batch email was sent for this operation
+ FakeEmailSender.Message message = Iterables.getOnlyElement(sender.getMessages());
+ assertThat(message.body())
+ .containsMatch(
+ Pattern.quote("Hello ")
+ + "("
+ + Pattern.quote(String.format("%s, %s", user.fullName(), user2.fullName()))
+ + "|"
+ + Pattern.quote(String.format("%s, %s", user2.fullName(), user.fullName()))
+ + ")");
+ assertThat(message.htmlBody())
+ .containsMatch(
+ "("
+ + Pattern.quote(String.format("%s and %s", user.fullName(), user2.fullName()))
+ + "|"
+ + Pattern.quote(String.format("%s and %s", user2.fullName(), user.fullName()))
+ + ")"
+ + Pattern.quote(" to <strong>review</strong> this change"));
+
+ // Ensure that a batch event has been sent:
+ // * 1 batch event for adding user and user2 as reviewers
+ assertThat(testReviewerAddedListener.receivedEvents).hasSize(1);
+ assertThat(testReviewerAddedListener.getReviewerIds()).containsExactly(user.id(), user2.id());
+ }
+
+ @Test
public void deletingReviewers() throws Exception {
PushOneCommit.Result r = createChange();
@@ -712,21 +771,25 @@
sender.clear();
- // remove user and user2
- ReviewResult reviewResult =
- gApi.changes()
- .id(r.getChangeId())
- .current()
- .review(
- ReviewInput.create()
- .reviewer(user.email(), ReviewerState.REMOVED, /* confirmed= */ true)
- .reviewer(user2.email(), ReviewerState.REMOVED, /* confirmed= */ true));
+ TestReviewerDeletedListener testReviewerDeletedListener = new TestReviewerDeletedListener();
+ try (Registration registration =
+ extensionRegistry.newRegistration().add(testReviewerDeletedListener)) {
+ // remove user and user2
+ ReviewResult reviewResult =
+ gApi.changes()
+ .id(r.getChangeId())
+ .current()
+ .review(
+ ReviewInput.create()
+ .reviewer(user.email(), ReviewerState.REMOVED, /* confirmed= */ true)
+ .reviewer(user2.email(), ReviewerState.REMOVED, /* confirmed= */ true));
- assertThat(
- reviewResult.reviewers.values().stream()
- .map(a -> a.removed.name)
- .collect(toImmutableSet()))
- .containsExactly(user.fullName(), user2.fullName());
+ assertThat(
+ reviewResult.reviewers.values().stream()
+ .map(a -> a.removed.name)
+ .collect(toImmutableSet()))
+ .containsExactly(user.fullName(), user2.fullName());
+ }
assertThat(gApi.changes().id(r.getChangeId()).reviewers()).isEmpty();
@@ -748,6 +811,12 @@
+ "|"
+ Pattern.quote(String.format("%s and %s", user2.fullName(), user.fullName()))
+ ")");
+
+ // Ensure that events have been sent:
+ // * 2 events for removing user and user2 as reviewers (one event per removed reviewer, batch
+ // event not available for reviewer removal)
+ assertThat(testReviewerDeletedListener.receivedEvents).hasSize(2);
+ assertThat(testReviewerDeletedListener.getReviewerIds()).containsExactly(user.id(), user2.id());
}
@Test
@@ -766,30 +835,38 @@
sender.clear();
- // remove user and user2 while adding user3 and user4
- ReviewResult reviewResult =
- gApi.changes()
- .id(r.getChangeId())
- .current()
- .review(
- ReviewInput.create()
- .reviewer(user.email(), ReviewerState.REMOVED, /* confirmed= */ true)
- .reviewer(user2.email(), ReviewerState.REMOVED, /* confirmed= */ true)
- .reviewer(user3.email())
- .reviewer(user4.email()));
+ TestReviewerAddedListener testReviewerAddedListener = new TestReviewerAddedListener();
+ TestReviewerDeletedListener testReviewerDeletedListener = new TestReviewerDeletedListener();
+ try (Registration registration =
+ extensionRegistry
+ .newRegistration()
+ .add(testReviewerAddedListener)
+ .add(testReviewerDeletedListener)) {
+ // remove user and user2 while adding user3 and user4
+ ReviewResult reviewResult =
+ gApi.changes()
+ .id(r.getChangeId())
+ .current()
+ .review(
+ ReviewInput.create()
+ .reviewer(user.email(), ReviewerState.REMOVED, /* confirmed= */ true)
+ .reviewer(user2.email(), ReviewerState.REMOVED, /* confirmed= */ true)
+ .reviewer(user3.email())
+ .reviewer(user4.email()));
- assertThat(
- reviewResult.reviewers.values().stream()
- .filter(a -> a.removed != null)
- .map(a -> a.removed.name)
- .collect(toImmutableSet()))
- .containsExactly(user.fullName(), user2.fullName());
- assertThat(
- reviewResult.reviewers.values().stream()
- .filter(a -> a.reviewers != null)
- .map(a -> Iterables.getOnlyElement(a.reviewers).name)
- .collect(toImmutableSet()))
- .containsExactly(user3.fullName(), user4.fullName());
+ assertThat(
+ reviewResult.reviewers.values().stream()
+ .filter(a -> a.removed != null)
+ .map(a -> a.removed.name)
+ .collect(toImmutableSet()))
+ .containsExactly(user.fullName(), user2.fullName());
+ assertThat(
+ reviewResult.reviewers.values().stream()
+ .filter(a -> a.reviewers != null)
+ .map(a -> Iterables.getOnlyElement(a.reviewers).name)
+ .collect(toImmutableSet()))
+ .containsExactly(user3.fullName(), user4.fullName());
+ }
assertThat(
gApi.changes().id(r.getChangeId()).reviewers().stream()
@@ -832,6 +909,15 @@
+ "|"
+ Pattern.quote(String.format("%s and %s", user2.fullName(), user.fullName()))
+ ")");
+
+ // Ensure that events have been sent:
+ // * 1 batch event for adding user3 and user4 as reviewers
+ // * 2 events for removing user and user2 as reviewers (one event per removed reviewer, batch
+ // event not available for reviewer removal)
+ assertThat(testReviewerAddedListener.receivedEvents).hasSize(1);
+ assertThat(testReviewerAddedListener.getReviewerIds()).containsExactly(user3.id(), user4.id());
+ assertThat(testReviewerDeletedListener.receivedEvents).hasSize(2);
+ assertThat(testReviewerDeletedListener.getReviewerIds()).containsExactly(user.id(), user2.id());
}
@Test
@@ -964,4 +1050,36 @@
return Optional.empty();
}
}
+
+ private static class TestReviewerAddedListener implements ReviewerAddedListener {
+ List<ReviewerAddedListener.Event> receivedEvents = new ArrayList<>();
+
+ @Override
+ public void onReviewersAdded(ReviewerAddedListener.Event event) {
+ receivedEvents.add(event);
+ }
+
+ public ImmutableSet<Account.Id> getReviewerIds() {
+ return receivedEvents.stream()
+ .flatMap(e -> e.getReviewers().stream())
+ .map(accountInfo -> Account.id(accountInfo._accountId))
+ .collect(toImmutableSet());
+ }
+ }
+
+ private static class TestReviewerDeletedListener implements ReviewerDeletedListener {
+ List<ReviewerDeletedListener.Event> receivedEvents = new ArrayList<>();
+
+ @Override
+ public void onReviewerDeleted(ReviewerDeletedListener.Event event) {
+ receivedEvents.add(event);
+ }
+
+ public ImmutableSet<Account.Id> getReviewerIds() {
+ return receivedEvents.stream()
+ .map(ReviewerDeletedListener.Event::getReviewer)
+ .map(accountInfo -> Account.id(accountInfo._accountId))
+ .collect(toImmutableSet());
+ }
+ }
}
diff --git a/javatests/com/google/gerrit/acceptance/api/change/StickyApprovalsIT.java b/javatests/com/google/gerrit/acceptance/api/change/StickyApprovalsIT.java
index cd9e876..3888679 100644
--- a/javatests/com/google/gerrit/acceptance/api/change/StickyApprovalsIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/change/StickyApprovalsIT.java
@@ -43,6 +43,7 @@
import com.google.gerrit.acceptance.testsuite.change.ChangeOperations;
import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations;
+import com.google.gerrit.common.RawInputUtil;
import com.google.gerrit.entities.Change;
import com.google.gerrit.entities.LabelId;
import com.google.gerrit.entities.LabelType;
@@ -557,6 +558,46 @@
}
@Test
+ public void
+ stickyWithCopyAllScoresIfListOfFilesDidNotChangeWhenFileIsModifiedDueToRebase_withoutCopyCondition()
+ throws Exception {
+ try (ProjectConfigUpdate u = updateProject(project)) {
+ u.getConfig()
+ .updateLabelType(
+ LabelId.CODE_REVIEW, b -> b.setCopyAllScoresIfListOfFilesDidNotChange(true));
+ u.save();
+ }
+ // Create two changes both with the same parent
+ PushOneCommit.Result r = createChange();
+ testRepo.reset("HEAD~1");
+ PushOneCommit.Result r2 = createChange();
+
+ // Modify f.txt in change 1. Approve and submit the first change
+ gApi.changes().id(r.getChangeId()).edit().modifyFile("f.txt", RawInputUtil.create("content"));
+ gApi.changes().id(r.getChangeId()).edit().publish();
+ RevisionApi revision = gApi.changes().id(r.getChangeId()).current();
+ revision.review(ReviewInput.approve().label(LabelId.VERIFIED, 1));
+ revision.submit();
+
+ // Add an approval whose score should be copied on change 2.
+ gApi.changes().id(r2.getChangeId()).current().review(ReviewInput.recommend());
+
+ // Rebase the second change. The rebase adds f1.txt.
+ gApi.changes().id(r2.getChangeId()).rebase();
+
+ // The code-review approval is copied for the second change between PS1 and PS2 since the only
+ // modified file is due to rebase.
+ List<PatchSetApproval> patchSetApprovals =
+ r2.getChange().notes().getApprovalsWithCopied().values().stream()
+ .sorted(comparing(a -> a.patchSetId().get()))
+ .collect(toImmutableList());
+ PatchSetApproval nonCopied = patchSetApprovals.get(0);
+ PatchSetApproval copied = patchSetApprovals.get(1);
+ assertCopied(nonCopied, /* psId= */ 1, LabelId.CODE_REVIEW, (short) 1, false);
+ assertCopied(copied, /* psId= */ 2, LabelId.CODE_REVIEW, (short) 1, true);
+ }
+
+ @Test
public void stickyWithCopyAllScoresIfListOfFilesDidNotChangeWhenFileIsModified_withCopyCondition()
throws Exception {
try (ProjectConfigUpdate u = updateProject(project)) {
@@ -1072,17 +1113,9 @@
.sorted(comparing(a -> a.patchSetId().get()))
.collect(toImmutableList());
PatchSetApproval nonCopied = patchSetApprovals.get(0);
-
- assertThat(nonCopied.patchSetId().get()).isEqualTo(1);
- assertThat(nonCopied.label()).isEqualTo(LabelId.CODE_REVIEW);
- assertThat(nonCopied.value()).isEqualTo((short) 1);
- assertThat(nonCopied.copied()).isFalse();
-
PatchSetApproval copied = patchSetApprovals.get(1);
- assertThat(copied.patchSetId().get()).isEqualTo(2);
- assertThat(copied.label()).isEqualTo(LabelId.CODE_REVIEW);
- assertThat(copied.value()).isEqualTo((short) 1);
- assertThat(copied.copied()).isTrue();
+ assertCopied(nonCopied, 1, LabelId.CODE_REVIEW, (short) 1, /* copied= */ false);
+ assertCopied(copied, 2, LabelId.CODE_REVIEW, (short) 1, /* copied= */ true);
}
@Test
@@ -1311,4 +1344,12 @@
}
assertWithMessage(name).that(vote).isEqualTo(expectedVote);
}
+
+ private void assertCopied(
+ PatchSetApproval approval, int psId, String label, short value, boolean copied) {
+ assertThat(approval.patchSetId().get()).isEqualTo(psId);
+ assertThat(approval.label()).isEqualTo(label);
+ assertThat(approval.value()).isEqualTo(value);
+ assertThat(approval.copied()).isEqualTo(copied);
+ }
}
diff --git a/javatests/com/google/gerrit/acceptance/server/change/GetRelatedIT.java b/javatests/com/google/gerrit/acceptance/server/change/GetRelatedIT.java
index 74dfa04..e778a5c 100644
--- a/javatests/com/google/gerrit/acceptance/server/change/GetRelatedIT.java
+++ b/javatests/com/google/gerrit/acceptance/server/change/GetRelatedIT.java
@@ -45,9 +45,9 @@
import com.google.gerrit.extensions.common.CommitInfo;
import com.google.gerrit.extensions.common.EditInfo;
import com.google.gerrit.index.IndexConfig;
+import com.google.gerrit.server.change.GetRelatedChangesUtil;
import com.google.gerrit.server.query.change.ChangeData;
import com.google.gerrit.server.restapi.change.ChangesCollection;
-import com.google.gerrit.server.restapi.change.GetRelated;
import com.google.gerrit.server.update.BatchUpdate;
import com.google.gerrit.server.update.BatchUpdateOp;
import com.google.gerrit.server.update.ChangeContext;
@@ -573,7 +573,7 @@
ChangeData cd = getChange(last);
assertThat(cd.patchSets()).hasSize(n);
- assertThat(GetRelated.getAllGroups(cd.notes(), psUtil)).hasSize(n);
+ assertThat(GetRelatedChangesUtil.getAllGroups(cd.notes().getPatchSets().values())).hasSize(n);
assertRelated(cd.change().currentPatchSetId());
}
diff --git a/javatests/com/google/gerrit/extensions/common/ChangeInfoDifferTest.java b/javatests/com/google/gerrit/extensions/common/ChangeInfoDifferTest.java
index 4352fe8..024e35e 100644
--- a/javatests/com/google/gerrit/extensions/common/ChangeInfoDifferTest.java
+++ b/javatests/com/google/gerrit/extensions/common/ChangeInfoDifferTest.java
@@ -58,6 +58,17 @@
}
@Test
+ public void getDiff_returnsOldAndNewChangeInfos() {
+ ChangeInfo oldChangeInfo = createChangeInfoWithTopic("topic");
+ ChangeInfo newChangeInfo = createChangeInfoWithTopic(oldChangeInfo.topic);
+
+ ChangeInfoDifference diff = ChangeInfoDiffer.getDifference(oldChangeInfo, newChangeInfo);
+
+ assertThat(diff.oldChangeInfo()).isEqualTo(oldChangeInfo);
+ assertThat(diff.newChangeInfo()).isEqualTo(newChangeInfo);
+ }
+
+ @Test
public void getDiff_givenUnchangedTopic_returnsNullTopics() {
ChangeInfo oldChangeInfo = createChangeInfoWithTopic("topic");
ChangeInfo newChangeInfo = createChangeInfoWithTopic(oldChangeInfo.topic);
diff --git a/javatests/com/google/gerrit/server/notedb/CommitRewriterTest.java b/javatests/com/google/gerrit/server/notedb/CommitRewriterTest.java
index 2ea1d7c..056c7dc 100644
--- a/javatests/com/google/gerrit/server/notedb/CommitRewriterTest.java
+++ b/javatests/com/google/gerrit/server/notedb/CommitRewriterTest.java
@@ -34,6 +34,7 @@
import com.google.gerrit.entities.RefNames;
import com.google.gerrit.entities.SubmitRecord;
import com.google.gerrit.json.OutputFormat;
+import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.ReviewerStatusUpdate;
import com.google.gerrit.server.notedb.ChangeNoteUtil.AttentionStatusInNoteDb;
@@ -784,7 +785,7 @@
.containsExactly(
"@@ -6 +6 @@\n"
+ "-Removed Verified+2 by Other Account <other@account.com>\n"
- + "+Removed Verified+2 by Gerrit Account\n");
+ + "+Removed Verified+2\n");
BackfillResult secondRunResult = rewriter.backfillProject(project, repo, options);
assertThat(secondRunResult.fixedRefDiff.keySet()).isEmpty();
assertThat(secondRunResult.refsFailedToFix).isEmpty();
@@ -837,6 +838,118 @@
}
@Test
+ public void fixRemoveVoteChangeMessageWithNoFooterLabel_matchByEmail() throws Exception {
+ Change c = newChange();
+ ChangeUpdate approvalUpdate = newUpdate(c, changeOwner);
+ approvalUpdate.putApproval(VERIFIED, (short) +2);
+
+ approvalUpdate.putApprovalFor(otherUserId, VERIFIED, (short) -1);
+ approvalUpdate.commit();
+ writeUpdate(
+ RefNames.changeMetaRef(c.getId()),
+ getChangeUpdateBody(
+ c, /*changeMessage=*/ "Removed Verified+2 by Renamed Change Owner <change@owner.com>"),
+ getAuthorIdent(changeOwner.getAccount()));
+
+ RunOptions options = new RunOptions();
+ options.dryRun = false;
+ BackfillResult result = rewriter.backfillProject(project, repo, options);
+ assertThat(result.fixedRefDiff.keySet()).containsExactly(RefNames.changeMetaRef(c.getId()));
+
+ List<String> commitHistoryDiff = commitHistoryDiff(result, c.getId());
+ assertThat(commitHistoryDiff)
+ .containsExactly(
+ "@@ -6 +6 @@\n"
+ + "-Removed Verified+2 by Renamed Change Owner <change@owner.com>\n"
+ + "+Removed Verified+2 by <GERRIT_ACCOUNT_1>\n");
+ BackfillResult secondRunResult = rewriter.backfillProject(project, repo, options);
+ assertThat(secondRunResult.fixedRefDiff.keySet()).isEmpty();
+ assertThat(secondRunResult.refsFailedToFix).isEmpty();
+ }
+
+ @Test
+ public void fixRemoveVoteChangeMessageWithNoFooterLabel_matchByName() throws Exception {
+ Change c = newChange();
+ ChangeUpdate approvalUpdate = newUpdate(c, changeOwner);
+ approvalUpdate.putApproval(VERIFIED, (short) +2);
+
+ approvalUpdate.putApprovalFor(otherUserId, VERIFIED, (short) -1);
+ approvalUpdate.commit();
+ writeUpdate(
+ RefNames.changeMetaRef(c.getId()),
+ getChangeUpdateBody(c, /*changeMessage=*/ "Removed Verified+2 by Change Owner"),
+ getAuthorIdent(changeOwner.getAccount()));
+
+ RunOptions options = new RunOptions();
+ options.dryRun = false;
+ BackfillResult result = rewriter.backfillProject(project, repo, options);
+ assertThat(result.fixedRefDiff.keySet()).containsExactly(RefNames.changeMetaRef(c.getId()));
+
+ List<String> commitHistoryDiff = commitHistoryDiff(result, c.getId());
+ assertThat(commitHistoryDiff)
+ .containsExactly(
+ "@@ -6 +6 @@\n"
+ + "-Removed Verified+2 by Change Owner\n"
+ + "+Removed Verified+2 by <GERRIT_ACCOUNT_1>\n");
+ BackfillResult secondRunResult = rewriter.backfillProject(project, repo, options);
+ assertThat(secondRunResult.fixedRefDiff.keySet()).isEmpty();
+ assertThat(secondRunResult.refsFailedToFix).isEmpty();
+ }
+
+ @Test
+ public void fixRemoveVoteChangeMessageWithNoFooterLabel_matchDuplicateAccounts()
+ throws Exception {
+ Account duplicateCodeOwner =
+ Account.builder(Account.id(4), TimeUtil.nowTs())
+ .setFullName(changeOwner.getName())
+ .setPreferredEmail("other@test.com")
+ .build();
+ accountCache.put(duplicateCodeOwner);
+ Change c = newChange();
+ ChangeUpdate approvalUpdate = newUpdate(c, changeOwner);
+ approvalUpdate.putApproval(VERIFIED, (short) +2);
+
+ approvalUpdate.putApprovalFor(duplicateCodeOwner.id(), VERIFIED, (short) -1);
+ approvalUpdate.commit();
+ writeUpdate(
+ RefNames.changeMetaRef(c.getId()),
+ getChangeUpdateBody(
+ c, /*changeMessage=*/ "Removed Verified+2 by Change Owner <other@test.com>"),
+ getAuthorIdent(changeOwner.getAccount()));
+ writeUpdate(
+ RefNames.changeMetaRef(c.getId()),
+ getChangeUpdateBody(
+ c, /*changeMessage=*/ "Removed Verified+2 by Change Owner <change@owner.com>"),
+ getAuthorIdent(changeOwner.getAccount()));
+ writeUpdate(
+ RefNames.changeMetaRef(c.getId()),
+ getChangeUpdateBody(
+ c, /*changeMessage=*/ "Removed Verified-1 by Change Owner <other@test.com>"),
+ getAuthorIdent(changeOwner.getAccount()));
+
+ RunOptions options = new RunOptions();
+ options.dryRun = false;
+ BackfillResult result = rewriter.backfillProject(project, repo, options);
+ assertThat(result.fixedRefDiff.keySet()).containsExactly(RefNames.changeMetaRef(c.getId()));
+
+ List<String> commitHistoryDiff = commitHistoryDiff(result, c.getId());
+ assertThat(commitHistoryDiff)
+ .containsExactly(
+ "@@ -6 +6 @@\n"
+ + "-Removed Verified+2 by Change Owner <other@test.com>\n"
+ + "+Removed Verified+2 by <GERRIT_ACCOUNT_4>\n",
+ "@@ -6 +6 @@\n"
+ + "-Removed Verified+2 by Change Owner <change@owner.com>\n"
+ + "+Removed Verified+2 by <GERRIT_ACCOUNT_1>\n",
+ "@@ -6 +6 @@\n"
+ + "-Removed Verified-1 by Change Owner <other@test.com>\n"
+ + "+Removed Verified-1 by <GERRIT_ACCOUNT_4>\n");
+ BackfillResult secondRunResult = rewriter.backfillProject(project, repo, options);
+ assertThat(secondRunResult.fixedRefDiff.keySet()).isEmpty();
+ assertThat(secondRunResult.refsFailedToFix).isEmpty();
+ }
+
+ @Test
public void fixRemoveVotesChangeMessage() throws Exception {
Change c = newChange();
ChangeUpdate approvalUpdate = newUpdate(c, changeOwner);
@@ -1491,19 +1604,20 @@
accountCache.put(duplicateReviewer);
Change c = newChange();
ImmutableList.Builder<ObjectId> commitsToFix = new ImmutableList.Builder<>();
- ChangeUpdate addReviewerUpdate = newUpdate(c, changeOwner);
+ ChangeUpdate addReviewerUpdate = newCodeOwnerAddReviewerUpdate(c, changeOwner);
addReviewerUpdate.putReviewer(reviewer.id(), REVIEWER);
addReviewerUpdate.commit();
- ChangeUpdate invalidOnAddReviewerUpdate = newUpdate(c, changeOwner);
+ ChangeUpdate invalidOnAddReviewerUpdate = newCodeOwnerAddReviewerUpdate(c, changeOwner);
invalidOnAddReviewerUpdate.setChangeMessage(
"Reviewer User who was added as reviewer owns the following files:\n"
+ " * file1.java\n"
+ " * file2.ts\n");
commitsToFix.add(invalidOnAddReviewerUpdate.commit());
- ChangeUpdate addOtherReviewerUpdate = newUpdate(c, changeOwner);
+ ChangeUpdate addOtherReviewerUpdate = newCodeOwnerAddReviewerUpdate(c, changeOwner);
addOtherReviewerUpdate.putReviewer(otherUserId, REVIEWER);
addOtherReviewerUpdate.commit();
- ChangeUpdate invalidOnAddReviewerMultipleReviewerUpdate = newUpdate(c, changeOwner);
+ ChangeUpdate invalidOnAddReviewerMultipleReviewerUpdate =
+ newCodeOwnerAddReviewerUpdate(c, changeOwner);
invalidOnAddReviewerMultipleReviewerUpdate.setChangeMessage(
"Reviewer User who was added as reviewer owns the following files:\n"
+ " * file1.java\n"
@@ -1512,17 +1626,17 @@
+ "\nMissing Reviewer who was added as reviewer owns the following files:\n"
+ " * file4.java\n");
commitsToFix.add(invalidOnAddReviewerMultipleReviewerUpdate.commit());
- ChangeUpdate addDuplicateReviewerUpdate = newUpdate(c, changeOwner);
+ ChangeUpdate addDuplicateReviewerUpdate = newCodeOwnerAddReviewerUpdate(c, changeOwner);
addDuplicateReviewerUpdate.putReviewer(duplicateReviewer.id(), REVIEWER);
addDuplicateReviewerUpdate.commit();
// Reviewer name resolves to multiple accounts in the same change
- ChangeUpdate onAddReviewerUpdateWithDuplicate = newUpdate(c, changeOwner);
+ ChangeUpdate onAddReviewerUpdateWithDuplicate = newCodeOwnerAddReviewerUpdate(c, changeOwner);
onAddReviewerUpdateWithDuplicate.setChangeMessage(
"Reviewer User who was added as reviewer owns the following files:\n"
+ " * file6.java\n");
commitsToFix.add(onAddReviewerUpdateWithDuplicate.commit());
- ChangeUpdate validOnAddReviewerUpdate = newUpdate(c, changeOwner);
+ ChangeUpdate validOnAddReviewerUpdate = newCodeOwnerAddReviewerUpdate(c, changeOwner);
validOnAddReviewerUpdate.setChangeMessage(
"Gerrit Account who was added as reviewer owns the following files:\n"
+ " * file1.java\n"
@@ -1557,10 +1671,9 @@
+ " * file1.java\n"
+ "\n<GERRIT_ACCOUNT_2>, who was added as reviewer owns the following files:\n"
+ " * file3.js\n"
- + "\nGerrit Account, who was added as reviewer owns the following files:\n"
+ + "\nAdded reviewer owns the following files:\n"
+ " * file4.java\n",
- "Gerrit Account, who was added as reviewer owns the following files:\n"
- + " * file6.java\n",
+ "Added reviewer owns the following files:\n" + " * file6.java\n",
"Gerrit Account who was added as reviewer owns the following files:\n"
+ " * file1.java\n"
+ "\n<GERRIT_ACCOUNT_1> who was added as reviewer owns the following files:\n"
@@ -1587,10 +1700,10 @@
+ "+<GERRIT_ACCOUNT_2>, who was added as reviewer owns the following files:\n"
+ "@@ -12 +12 @@\n"
+ "-Missing Reviewer who was added as reviewer owns the following files:\n"
- + "+Gerrit Account, who was added as reviewer owns the following files:\n",
+ + "+Added reviewer owns the following files:\n",
"@@ -6 +6 @@\n"
+ "-Reviewer User who was added as reviewer owns the following files:\n"
- + "+Gerrit Account, who was added as reviewer owns the following files:\n");
+ + "+Added reviewer owns the following files:\n");
BackfillResult secondRunResult = rewriter.backfillProject(project, repo, options);
assertThat(secondRunResult.fixedRefDiff.keySet()).isEmpty();
assertThat(secondRunResult.refsFailedToFix).isEmpty();
@@ -1937,7 +2050,8 @@
getChangeUpdateBody(
c,
String.format(
- "Assignee changed from: %s to: %s", changeOwner.getName(), otherUser.getName())),
+ "Assignee changed from: %s to: %s",
+ changeOwner.getNameEmail(), otherUser.getNameEmail())),
getAuthorIdent(otherUser.getAccount()));
writeUpdate(
RefNames.changeMetaRef(c.getId()),
@@ -1972,14 +2086,12 @@
+ "-Assignee added: Change Owner\n"
+ "+Assignee added: <GERRIT_ACCOUNT_1>\n",
"@@ -6 +6 @@\n"
- + "-Assignee changed from: Change Owner to: Other Account\n"
+ + "-Assignee changed from: Change Owner <change@owner.com> to: Other Account <other@account.com>\n"
+ "+Assignee changed from: <GERRIT_ACCOUNT_1> to: <GERRIT_ACCOUNT_2>\n",
"@@ -6 +6 @@\n"
+ "-Assignee deleted: Other Account\n"
+ "+Assignee deleted: <GERRIT_ACCOUNT_2>\n",
- "@@ -6 +6 @@\n"
- + "-Assignee added: Reviewer User\n"
- + "+Assignee added: Gerrit Account\n");
+ "@@ -6 +6 @@\n" + "-Assignee added: Reviewer User\n" + "+Assignee was added.\n");
BackfillResult secondRunResult = rewriter.backfillProject(project, repo, options);
assertThat(secondRunResult.fixedRefDiff.keySet()).isEmpty();
assertThat(secondRunResult.refsFailedToFix).isEmpty();
@@ -2137,6 +2249,13 @@
.collect(toImmutableList());
}
+ protected ChangeUpdate newCodeOwnerAddReviewerUpdate(Change c, CurrentUser user)
+ throws Exception {
+ ChangeUpdate update = newUpdate(c, user, true);
+ update.setTag("autogenerated:gerrit:code-owners:addReviewer");
+ return update;
+ }
+
private ImmutableList<String> commitHistoryDiff(BackfillResult result, Change.Id changeId) {
return result.fixedRefDiff.get(RefNames.changeMetaRef(changeId)).stream()
.map(CommitDiff::diff)
diff --git a/javatests/com/google/gerrit/server/patch/PatchListTest.java b/javatests/com/google/gerrit/server/patch/PatchListTest.java
deleted file mode 100644
index 182ce49..0000000
--- a/javatests/com/google/gerrit/server/patch/PatchListTest.java
+++ /dev/null
@@ -1,97 +0,0 @@
-// Copyright (C) 2017 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.patch;
-
-import static com.google.common.truth.Truth.assertThat;
-
-import com.google.gerrit.entities.Patch;
-import com.google.gerrit.entities.Patch.ChangeType;
-import com.google.gerrit.server.patch.PatchList.ChangeTypeCmp;
-import java.io.ByteArrayInputStream;
-import java.io.ByteArrayOutputStream;
-import java.io.InputStream;
-import java.io.ObjectInputStream;
-import java.io.ObjectOutputStream;
-import java.util.Arrays;
-import java.util.List;
-import org.junit.Test;
-
-public class PatchListTest {
-
- @Test
- public void fileOrder() {
- String[] names = {
- "zzz",
- "def/g",
- "/!xxx",
- "abc",
- Patch.MERGE_LIST,
- "qrx",
- Patch.COMMIT_MSG,
- Patch.PATCHSET_LEVEL
- };
- String[] want = {
- Patch.COMMIT_MSG,
- Patch.MERGE_LIST,
- Patch.PATCHSET_LEVEL,
- "/!xxx",
- "abc",
- "def/g",
- "qrx",
- "zzz",
- };
- Arrays.sort(names, 0, names.length, PatchList.FILE_PATH_CMP);
- assertThat(names).isEqualTo(want);
- }
-
- @Test
- public void fileOrderNoMerge() {
- String[] names = {
- "zzz", "def/g", "/!xxx", "abc", "qrx", Patch.COMMIT_MSG,
- };
- String[] want = {
- Patch.COMMIT_MSG, "/!xxx", "abc", "def/g", "qrx", "zzz",
- };
-
- Arrays.sort(names, 0, names.length, PatchList.FILE_PATH_CMP);
- assertThat(names).isEqualTo(want);
- }
-
- @Test
- public void changeTypeOrderIsComplete() {
- List<ChangeType> changeTypeOrder = ChangeTypeCmp.order;
- ChangeType[] allTypes = ChangeType.values();
-
- Arrays.sort(allTypes, PatchList.CHANGE_TYPE_CMP);
- assertThat(changeTypeOrder).containsExactlyElementsIn(allTypes).inOrder();
- }
-
- @Test
- public void largeObjectTombstoneCanBeSerializedAndDeserialized() throws Exception {
- // Serialize
- byte[] serializedObject;
- try (ByteArrayOutputStream baos = new ByteArrayOutputStream();
- ObjectOutputStream objectStream = new ObjectOutputStream(baos)) {
- objectStream.writeObject(new PatchListCacheImpl.LargeObjectTombstone());
- serializedObject = baos.toByteArray();
- assertThat(serializedObject).isNotNull();
- }
- // Deserialize
- try (InputStream is = new ByteArrayInputStream(serializedObject);
- ObjectInputStream ois = new ObjectInputStream(is)) {
- assertThat(ois.readObject()).isInstanceOf(PatchListCacheImpl.LargeObjectTombstone.class);
- }
- }
-}
diff --git a/javatests/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java b/javatests/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java
index c8949e6..9ebee9c 100644
--- a/javatests/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java
+++ b/javatests/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java
@@ -3698,18 +3698,16 @@
TestRepository<Repo> repo = createProject("repo");
Change change = insert(repo, newChange(repo));
- AssigneeInput ain = new AssigneeInput();
- ain.assignee = user2.toString();
- gApi.changes().id(change.getId().get()).setAssignee(ain);
+ gApi.changes().id(change.getId().get()).addReviewer(user2.toString());
RequestContext adminContext = requestContext.setContext(newRequestContext(user2));
- assertQuery("assignee:self", change);
+ assertQuery("reviewer:self", change);
requestContext.setContext(adminContext);
gApi.accounts().id(user2.get()).setActive(false);
requestContext.setContext(newRequestContext(user2));
- assertQuery("assignee:self", change);
+ assertQuery("reviewer:self", change);
}
@Test
diff --git a/package.json b/package.json
index c3dfad0..a492055 100644
--- a/package.json
+++ b/package.json
@@ -6,7 +6,7 @@
"@bazel/rollup": "^3.5.0",
"@bazel/terser": "^3.5.0",
"@bazel/typescript": "^3.5.0",
- "twinkie": "^1.1.2"
+ "twinkie": "^1.1.3"
},
"devDependencies": {
"@typescript-eslint/eslint-plugin": "^4.29.0",
@@ -15,12 +15,16 @@
"eslint-plugin-html": "^6.1.2",
"eslint-plugin-import": "^2.22.1",
"eslint-plugin-jsdoc": "^32.3.0",
+ "eslint-plugin-lit": "^1.5.1",
"eslint-plugin-node": "^11.1.0",
"eslint-plugin-prettier": "^3.4.0",
+ "eslint-plugin-regex": "^1.8.0",
"gts": "^3.1.0",
+ "lit-analyzer": "^1.2.1",
"prettier": "2.3.1",
"rollup": "^2.45.2",
"terser": "^5.6.1",
+ "ts-lit-plugin": "^1.2.1",
"typescript": "4.3.2"
},
"scripts": {
@@ -32,19 +36,19 @@
"safe_bazelisk": "if which bazelisk >/dev/null; then bazel_bin=bazelisk; else bazel_bin=bazel; fi && $bazel_bin",
"eslint": "npm run safe_bazelisk test polygerrit-ui/app:lint_test",
"eslintfix": "npm run safe_bazelisk run polygerrit-ui/app:lint_bin -- -- --fix $(pwd)/polygerrit-ui/app",
- "polylint": "npm run safe_bazelisk test polygerrit-ui/app:polylint_test",
+ "litlint": "npm run safe_bazelisk run polygerrit-ui/app:lit_analysis",
"test:debug": "npm run compile:local && npm run safe_bazelisk run //polygerrit-ui:karma_bin -- -- start $(pwd)/polygerrit-ui/karma.conf.js --root '.ts-out/polygerrit-ui/app/' --browsers ChromeDev --no-single-run --test-files",
"test:single": "npm run compile:local && npm run safe_bazelisk run //polygerrit-ui:karma_bin -- -- start $(pwd)/polygerrit-ui/karma.conf.js --root '.ts-out/polygerrit-ui/app/' --test-files",
- "postinstall": "(git apply --reverse --ignore-whitespace twinkie.patch || true) && git apply --ignore-whitespace twinkie.patch",
- "polytest": "npm run safe_bazelisk test //polygerrit-ui/app:validate_polymer_templates",
- "polytest:dev": "rm -rf ./polygerrit-ui/app/tmpl_out && npm run safe_bazelisk build //polygerrit-ui/app:template_test_tar && mkdir ./polygerrit-ui/app/tmpl_out && tar -xf bazel-bin/polygerrit-ui/app/template_test_tar.tar -C ./polygerrit-ui/app/tmpl_out"
+ "polylint": "npm run safe_bazelisk test //polygerrit-ui/app:polylint_test",
+ "polylint:dev": "rm -rf ./polygerrit-ui/app/tmpl_out && npm run safe_bazelisk build //polygerrit-ui/app:template_test_tar && mkdir ./polygerrit-ui/app/tmpl_out && tar -xf bazel-bin/polygerrit-ui/app/template_test_tar.tar -C ./polygerrit-ui/app/tmpl_out"
},
"repository": {
"type": "git",
"url": "https://gerrit.googlesource.com/gerrit"
},
"resolutions": {
- "lodash": "4.17.21"
+ "lodash": "4.17.21",
+ "twinkie/typescript": "4.3.2"
},
"author": "",
"license": "Apache-2.0"
diff --git a/plugins/BUILD b/plugins/BUILD
index 01ede06..0e5df2c 100644
--- a/plugins/BUILD
+++ b/plugins/BUILD
@@ -73,6 +73,7 @@
"//lib/commons:compress",
"//lib/commons:dbcp",
"//lib/commons:lang",
+ "//lib/commons:lang3",
"//lib/dropwizard:dropwizard-core",
"//lib/flogger:api",
"//lib/guice:guice",
diff --git a/plugins/delete-project b/plugins/delete-project
index 0b5bcbf..8fe544a 160000
--- a/plugins/delete-project
+++ b/plugins/delete-project
@@ -1 +1 @@
-Subproject commit 0b5bcbfc26d9c0559312ced40bb36a37f517b793
+Subproject commit 8fe544ac569efa357ee054257143d8e1d4aa6afd
diff --git a/plugins/plugin-manager b/plugins/plugin-manager
index 5b87f63..44808dc 160000
--- a/plugins/plugin-manager
+++ b/plugins/plugin-manager
@@ -1 +1 @@
-Subproject commit 5b87f63f3e9c5817bcddf008c0b4005494059368
+Subproject commit 44808dcad3c978ed12bb2cb454c6ad320912aa8a
diff --git a/plugins/tsconfig-plugins-base.json b/plugins/tsconfig-plugins-base.json
index 97eae67..b7e9d52 100644
--- a/plugins/tsconfig-plugins-base.json
+++ b/plugins/tsconfig-plugins-base.json
@@ -20,6 +20,7 @@
"noUnusedLocals": true, /* Report errors on unused locals. */
"noUnusedParameters": true, /* Report errors on unused parameters. */
"noImplicitReturns": true, /* Report error when not all code paths in function return a value. */
+ "noImplicitOverride": true,
"noFallthroughCasesInSwitch": true,/* Report errors for fallthrough cases in switch statement. */
"skipLibCheck": true, /* Do not check node_modules */
diff --git a/polygerrit-ui/app/.eslintrc.js b/polygerrit-ui/app/.eslintrc.js
index faf126c..14f9e8c 100644
--- a/polygerrit-ui/app/.eslintrc.js
+++ b/polygerrit-ui/app/.eslintrc.js
@@ -277,6 +277,18 @@
},
},
{
+ files: ['**/api/*.ts'],
+ rules: {
+ 'regex/invalid': [
+ 'error', [{
+ regex: 'export interface',
+ message: 'All interfaces in the api/ dir must have "declare"',
+ replacement: 'export declare interface',
+ }],
+ ],
+ },
+ },
+ {
files: ['**/*.ts'],
extends: [require.resolve('gts/.eslintrc.json')],
rules: {
@@ -400,12 +412,32 @@
}],
},
},
+ {
+ files: ['*.ts'],
+ excludedFiles: '*_html.ts',
+ rules: {
+ 'lit/attribute-value-entities': 'error',
+ 'lit/binding-positions': 'error',
+ 'lit/no-duplicate-template-bindings': 'error',
+ 'lit/no-invalid-html': 'error',
+ 'lit/no-legacy-template-syntax': 'error',
+ 'lit/no-property-change-update': 'error',
+ 'lit/no-invalid-escape-sequences': 'error',
+ 'lit/no-legacy-imports': 'error',
+ 'lit/no-private-properties': 'error',
+ 'lit/no-useless-template-literals': 'error',
+ 'lit/no-value-attribute': 'error',
+ 'lit/prefer-static-styles': 'error',
+ },
+ },
],
plugins: [
'html',
'jsdoc',
'import',
+ 'lit',
'prettier',
+ 'regex',
],
settings: {
'html/report-bad-indent': 'error',
diff --git a/polygerrit-ui/app/BUILD b/polygerrit-ui/app/BUILD
index 613efd6..3a647d4 100644
--- a/polygerrit-ui/app/BUILD
+++ b/polygerrit-ui/app/BUILD
@@ -45,8 +45,6 @@
),
allow_js = True,
incremental = True,
- # The same outdir also appears in the following files:
- # polylint_test.sh
out_dir = "_pg_ts_out",
tsc = "//tools/node_tools:tsc-bin",
tsconfig = ":ts_config_bazel",
@@ -96,9 +94,7 @@
# so template tests pass.
# TODO: fix problems reported by template checker in these files.
ignore_templates_list = [
- "elements/admin/gr-access-section/gr-access-section_html.ts",
"elements/admin/gr-admin-view/gr-admin-view_html.ts",
- "elements/admin/gr-create-change-dialog/gr-create-change-dialog_html.ts",
"elements/admin/gr-create-repo-dialog/gr-create-repo-dialog_html.ts",
"elements/admin/gr-group-members/gr-group-members_html.ts",
"elements/admin/gr-group/gr-group_html.ts",
@@ -109,7 +105,6 @@
"elements/admin/gr-repo-plugin-config/gr-repo-plugin-config_html.ts",
"elements/admin/gr-repo/gr-repo_html.ts",
"elements/admin/gr-rule-editor/gr-rule-editor_html.ts",
- "elements/change-list/gr-change-list-item/gr-change-list-item_html.ts",
"elements/change-list/gr-change-list-view/gr-change-list-view_html.ts",
"elements/change-list/gr-change-list/gr-change-list_html.ts",
"elements/change-list/gr-dashboard-view/gr-dashboard-view_html.ts",
@@ -117,9 +112,6 @@
"elements/change/gr-change-metadata/gr-change-metadata_html.ts",
"elements/change/gr-change-requirements/gr-change-requirements_html.ts",
"elements/change/gr-change-view/gr-change-view_html.ts",
- "elements/change/gr-confirm-abandon-dialog/gr-confirm-abandon-dialog_html.ts",
- "elements/change/gr-confirm-cherrypick-dialog/gr-confirm-cherrypick-dialog_html.ts",
- "elements/change/gr-confirm-move-dialog/gr-confirm-move-dialog_html.ts",
"elements/change/gr-file-list-header/gr-file-list-header_html.ts",
"elements/change/gr-file-list/gr-file-list_html.ts",
"elements/change/gr-label-score-row/gr-label-score-row_html.ts",
@@ -137,25 +129,21 @@
"elements/gr-app-element_html.ts",
"elements/settings/gr-watched-projects-editor/gr-watched-projects-editor_html.ts",
"elements/shared/gr-account-list/gr-account-list_html.ts",
- "elements/shared/gr-comment-thread/gr-comment-thread_html.ts",
- "elements/shared/gr-comment/gr-comment_html.ts",
- "elements/shared/gr-dropdown-list/gr-dropdown-list_html.ts",
- "elements/shared/gr-hovercard-account/gr-hovercard-account_html.ts",
- "elements/shared/gr-label-info/gr-label-info_html.ts",
- "elements/shared/gr-list-view/gr-list-view_html.ts",
]
+sources_for_template_checking = glob(
+ [src_dir + "/**/*" + ext for src_dir in src_dirs for ext in [
+ ".ts",
+ ]],
+ exclude = [
+ "**/*_test.ts",
+ ] + ignore_templates_list,
+)
+
# Transform templates into a .ts files.
templates_srcs = transform_polymer_templates(
name = "template_test",
- srcs = glob(
- [src_dir + "/**/*" + ext for src_dir in src_dirs for ext in [
- ".ts",
- ]],
- exclude = [
- "**/*_test.ts",
- ] + ignore_templates_list,
- ),
+ srcs = sources_for_template_checking,
out_tsconfig = "tsconfig_template_test.json",
tsconfig = "tsconfig_bazel.json",
deps = [
@@ -165,50 +153,34 @@
],
)
-# Compile transformed templates together with the polygerrit source. If
-# templates don't have problem, then the compilation ends without error.
-# Otherwise, the typescript compiler reports the error.
-# Note, that the compile_ts macro creates build rules. If the build succeed,
-# the macro creates the file compile_template_test.success. The
-# 'validate_polymer_templates' rule tests existence of the file.
-#
-# TODO: Re-instantiate this rule. It broke when switching to ts_project with
-# ERROR: //polygerrit-ui/app:compile_template_test srcs cannot be a mix of
-# generated files and source files since this would prevent giving a
-# single rootDir to the TypeScript compiler
-# Also, the emitJS feature of compile_ts has to be re-created in some form.
-#ts_project(
-# name = "compile_template_test",
-# srcs = templates_srcs + glob(
-# [src_dir + "/**/*" + ext for src_dir in src_dirs for ext in [
-# ".ts",
-# ]],
-# exclude = [
-# "**/*_test.ts",
-# ] + ignore_templates_list,
-# ),
-# allow_js = True,
-# out_dir = "_pg_template_test_out",
-# # Should not run sandboxed.
-# tags = [
-# "local",
-# "manual",
-# ],
-# tsc = "//tools/node_tools:tsc-bin",
-# tsconfig = "tsconfig_template_test.json",
-#)
-#
-# This rule allows to run polymer template checker with bazel test command.
-# For details - see compile_template_test rule.
-#
-# TODO: Re-instantiate this test. It broke when switching
-# 'compile_template_test'to ts_project, see above. ts_project does not
-# create '.success' files.
-#sh_test(
-# name = "validate_polymer_templates",
-# srcs = [":empty_test.sh"],
-# data = ["compile_template_test.success"],
-#)
+# After templates are converted into a typescript code, the TS compiler should check that the
+# converted code doesn't have the error (i.e. templates don't have problems).
+# The input to the compiler is: the converted (i.e. autogenerated) code + original polygerrit code;
+# the output (i.e. js code) is not needed (we only care wheather the code has error or not).
+# The existing ts_project rule can't compile a mix of a generated and a non-generated code, so it
+# can't be used for the purpose of template checking.
+# Because the output of TS compiler is not needed, the simplest workaround is to run typescript
+# compiler from command line using the sh_test rule. The compiler exits with non-zero return code if
+# errors found and sh_test fails.
+sh_test(
+ name = "polylint_test",
+ srcs = [":compile_generated_templates.sh"],
+ args = [
+ "$(location //tools/node_tools:tsc-bin)",
+ "$(location tsconfig_template_test.json)",
+ ],
+ data = [
+ "tsconfig_template_test.json",
+ "tsconfig_bazel.json",
+ "tsconfig.json",
+ "//tools/node_tools:tsc-bin",
+ "@ui_npm//:node_modules",
+ ] + templates_srcs + sources_for_template_checking,
+ tags = [
+ "local",
+ "manual",
+ ],
+)
polygerrit_bundle(
name = "polygerrit_ui",
@@ -292,31 +264,30 @@
)
filegroup(
- name = "polylint-fg",
- srcs = [
- # Workaround for https://github.com/bazelbuild/bazel/issues/1305
+ name = "lit_analysis_src_code",
+ srcs = glob(
+ ["**/*.ts"],
+ exclude = [
+ "**/*_html.ts",
+ "**/*_test.ts",
+ ],
+ ) + [
+ "@ui_dev_npm//:node_modules",
"@ui_npm//:node_modules",
- # Polylinter can't check .ts files, run it on compiled srcs
- ":compile_pg",
],
)
-sh_test(
- name = "polylint_test",
- size = "large",
- srcs = ["polylint_test.sh"],
- args = [
- "$(location @tools_npm//polymer-cli/bin:polymer)",
- "$(location polymer.json)",
- ],
+nodejs_binary(
+ name = "lit_analysis",
data = [
- "polymer.json",
- ":polylint-fg",
- "@tools_npm//polymer-cli/bin:polymer",
+ ":lit_analysis_src_code",
+ "@npm//lit-analyzer",
],
- # Should not run sandboxed.
- tags = [
- "local",
- "manual",
+ entry_point = "@npm//:node_modules/lit-analyzer/cli.js",
+ templated_args = [
+ "**/elements/**/*.ts",
+ "--strict",
+ "--rules.no-property-visibility-mismatch off",
+ "--rules.no-incompatible-property-type off",
],
)
diff --git a/polygerrit-ui/app/api/diff.ts b/polygerrit-ui/app/api/diff.ts
index 1453fd0..ee579ff 100644
--- a/polygerrit-ui/app/api/diff.ts
+++ b/polygerrit-ui/app/api/diff.ts
@@ -53,30 +53,30 @@
}
/**
+ * Represents a "generic" text range in the code (e.g. text selection)
+ */
+interface TextRange {
+ /** first line of the range (1-based inclusive). */
+ start_line: number;
+ /** first column of the range (in the first line) (1-based inclusive). */
+ start_column: number;
+ /** last line of the range (1-based inclusive). */
+ end_line: number;
+ /** last column of the range (in the end line) (1-based inclusive). */
+ end_column: number;
+}
+
+/**
* Represents a syntax block in a code (e.g. method, function, class, if-else).
*/
export declare interface SyntaxBlock {
/** Name of the block (e.g. name of the method/class)*/
name: string;
- /** Where does this block syntatically starts and ends (line number and column).*/
- range: {
- /** first line of the block (1-based inclusive). */
- start_line: number;
- /**
- * column of the range start inside the first line (e.g. "{" character ending a function/method)
- * (1-based inclusive).
- */
- start_column: number;
- /**
- * last line of the block (1-based inclusive).
- */
- end_line: number;
- /**
- * column of the block end inside the end line (e.g. "}" character ending a function/method)
- * (1-based inclusive).
- */
- end_column: number;
- };
+ /**
+ * Where does this block syntatically starts and ends (line number and
+ * column).
+ */
+ range: TextRange;
/** Sub-blocks of the current syntax block (e.g. methods of a class) */
children: SyntaxBlock[];
}
@@ -210,15 +210,22 @@
}
/**
- * Listens to changes in token highlighting - when a new token starts or stopped being highlighted.
- * Examples:
- * - Token highlighted: ('myFunctionName', 12, [Element]).
- * - Token unhighlighted: (undefined, 0, undefined).
+ * Event details when a token is highlighted.
*/
-export type TokenHighlightedListener = (
- newHighlight: string | undefined,
- newLineNumber: number,
- hoveredElement?: Element
+export declare interface TokenHighlightEventDetails {
+ token: string;
+ element: Element;
+ side: Side;
+ range: TextRange;
+}
+
+/**
+ * Listens to changes in token highlighting - when a new token starts or stopped
+ * being highlighted. undefined is sent if the event is about a clear in
+ * highlighting.
+ */
+export type TokenHighlightListener = (
+ tokenHighlightEvent?: TokenHighlightEventDetails
) => void;
export declare interface ImageDiffPreferences {
diff --git a/polygerrit-ui/app/api/embed.ts b/polygerrit-ui/app/api/embed.ts
index fed724e..520aeec 100644
--- a/polygerrit-ui/app/api/embed.ts
+++ b/polygerrit-ui/app/api/embed.ts
@@ -24,7 +24,7 @@
DiffLayer,
GrAnnotation,
GrDiffCursor,
- TokenHighlightedListener,
+ TokenHighlightListener,
} from './diff';
declare global {
@@ -35,7 +35,7 @@
TokenHighlightLayer: {
new (
container?: HTMLElement,
- listener?: TokenHighlightedListener
+ listener?: TokenHighlightListener
): DiffLayer;
};
};
diff --git a/polygerrit-ui/app/api/gerrit.ts b/polygerrit-ui/app/api/gerrit.ts
index 8488961..2091eea 100644
--- a/polygerrit-ui/app/api/gerrit.ts
+++ b/polygerrit-ui/app/api/gerrit.ts
@@ -25,7 +25,7 @@
}
}
-export interface Gerrit {
+export declare interface Gerrit {
install(
callback: (plugin: PluginApi) => void,
opt_version?: string,
diff --git a/polygerrit-ui/app/api/hook.ts b/polygerrit-ui/app/api/hook.ts
index f8a6cc1..8cbb9d0 100644
--- a/polygerrit-ui/app/api/hook.ts
+++ b/polygerrit-ui/app/api/hook.ts
@@ -16,7 +16,7 @@
*/
import {ChangeInfo, ConfigInfo, RevisionInfo} from './rest-api';
-export interface GerritElementExtensions {
+export declare interface GerritElementExtensions {
content?: HTMLElement & {hidden?: boolean};
change?: ChangeInfo;
revision?: RevisionInfo;
diff --git a/polygerrit-ui/app/api/popup.ts b/polygerrit-ui/app/api/popup.ts
index 8d81831..d265ee6 100644
--- a/polygerrit-ui/app/api/popup.ts
+++ b/polygerrit-ui/app/api/popup.ts
@@ -17,9 +17,10 @@
export declare interface PopupPluginApi {
/**
- * Opens the popup, inserts it into DOM over current UI.
- * Creates the popup if not previously created. Creates popup content element,
- * if it was provided with constructor.
+ * Opens the popup, inserts it into the DOM over current UI.
+ * Creates the popup if not previously created. Creates and inserts the popup
+ * content element, if a `moduleName` was provided in the constructor.
+ * Otherwise you have to call `appendContent()` when the promise resolves.
*/
open(): Promise<PopupPluginApi>;
@@ -27,4 +28,10 @@
* Hides the popup.
*/
close(): void;
+
+ /**
+ * Appends the given element as a child to the popup. Only call this method
+ * when you have called `popup()` without a `moduleName`.
+ */
+ appendContent(el: HTMLElement): void;
}
diff --git a/polygerrit-ui/app/api/rest-api.ts b/polygerrit-ui/app/api/rest-api.ts
index 4a4b79d..f86e825 100644
--- a/polygerrit-ui/app/api/rest-api.ts
+++ b/polygerrit-ui/app/api/rest-api.ts
@@ -240,7 +240,7 @@
* The AccountDetailInfo entity contains detailed information about an account.
* https://gerrit-review.googlesource.com/Documentation/rest-api-accounts.html#account-detail-info
*/
-export interface AccountDetailInfo extends AccountInfo {
+export declare interface AccountDetailInfo extends AccountInfo {
registered_on: Timestamp;
}
@@ -249,7 +249,7 @@
* from the accounts section.
* https://gerrit-review.googlesource.com/Documentation/rest-api-config.html#accounts-config-info
*/
-export interface AccountsConfigInfo {
+export declare interface AccountsConfigInfo {
visibility: string;
default_display_name: DefaultDisplayNameConfig;
}
@@ -320,7 +320,7 @@
* configuration of the Gerrit server.
* https://gerrit-review.googlesource.com/Documentation/rest-api-config.html#auth-info
*/
-export interface AuthInfo {
+export declare interface AuthInfo {
auth_type: AuthType; // docs incorrectly names it 'type'
use_contributor_agreements?: boolean;
contributor_agreements?: ContributorAgreementInfo[];
@@ -356,7 +356,7 @@
* from the change section.
* https://gerrit-review.googlesource.com/Documentation/rest-api-config.html#change-config-info
*/
-export interface ChangeConfigInfo {
+export declare interface ChangeConfigInfo {
allow_blame?: boolean;
large_change: number;
update_delay: number;
@@ -464,14 +464,14 @@
* The CommentLinkInfo entity describes acommentlink.
* https://gerrit-review.googlesource.com/Documentation/rest-api-projects.html#commentlink-info
*/
-export interface CommentLinkInfo {
+export declare interface CommentLinkInfo {
match: string;
link?: string;
enabled?: boolean;
html?: string;
}
-export interface CommentLinks {
+export declare interface CommentLinks {
[name: string]: CommentLinkInfo;
}
@@ -492,7 +492,8 @@
resolve_conflicts_web_links?: WebLinkInfo[];
}
-export interface ConfigArrayParameterInfo extends ConfigParameterInfoBase {
+export declare interface ConfigArrayParameterInfo
+ extends ConfigParameterInfoBase {
type: ConfigParameterInfoType.ARRAY;
values: string[];
}
@@ -502,7 +503,7 @@
* project configuration.
* https://gerrit-review.googlesource.com/Documentation/rest-api-projects.html#config-info
*/
-export interface ConfigInfo {
+export declare interface ConfigInfo {
description?: string;
use_contributor_agreements?: InheritedBooleanInfo;
use_content_merge?: InheritedBooleanInfo;
@@ -525,7 +526,8 @@
reject_empty_commit?: InheritedBooleanInfo;
}
-export interface ConfigListParameterInfo extends ConfigParameterInfoBase {
+export declare interface ConfigListParameterInfo
+ extends ConfigParameterInfoBase {
type: ConfigParameterInfoType.LIST;
permitted_values?: string[];
}
@@ -539,7 +541,7 @@
* The ConfigParameterInfo entity describes a project configurationparameter.
* https://gerrit-review.googlesource.com/Documentation/rest-api-projects.html#config-parameter-info
*/
-export interface ConfigParameterInfoBase {
+export declare interface ConfigParameterInfoBase {
display_name?: string;
description?: string;
warning?: string;
@@ -554,7 +556,7 @@
}
// https://gerrit-review.googlesource.com/Documentation/rest-api-accounts.html#contributor-agreement-info
-export interface ContributorAgreementInfo {
+export declare interface ContributorAgreementInfo {
name: string;
description: string;
url: string;
@@ -584,7 +586,7 @@
* options.
* https://gerrit-review.googlesource.com/Documentation/rest-api-config.html#download-info
*/
-export interface DownloadInfo {
+export declare interface DownloadInfo {
schemes: SchemesInfoMap;
archives: string[];
}
@@ -594,7 +596,7 @@
* scheme and its commands.
* https://gerrit-review.googlesource.com/Documentation/rest-api-config.html
*/
-export interface DownloadSchemeInfo {
+export declare interface DownloadSchemeInfo {
url: string;
is_auth_required: boolean;
is_auth_supported: boolean;
@@ -634,7 +636,7 @@
* the gerrit section.
* https://gerrit-review.googlesource.com/Documentation/rest-api-config.html#gerrit-info
*/
-export interface GerritInfo {
+export declare interface GerritInfo {
all_projects: string; // Doc contains incorrect name
all_users: string; // Doc contains incorrect name
doc_search: boolean;
@@ -685,7 +687,7 @@
* Gerrit internal group, or an external group that is known to Gerrit.
* https://gerrit-review.googlesource.com/Documentation/rest-api-groups.html#group-info
*/
-export interface GroupInfo {
+export declare interface GroupInfo {
id: GroupId;
name?: GroupName;
url?: string;
@@ -706,7 +708,7 @@
* Options of the group.
* https://gerrit-review.googlesource.com/Documentation/rest-api-groups.html
*/
-export interface GroupOptionsInfo {
+export declare interface GroupOptionsInfo {
visible_to_all: boolean;
}
@@ -718,7 +720,7 @@
* A boolean value that can also be inherited.
* https://gerrit-review.googlesource.com/Documentation/rest-api-projects.html#inherited-boolean-info
*/
-export interface InheritedBooleanInfo {
+export declare interface InheritedBooleanInfo {
value: boolean;
configured_value: InheritedBooleanInfoConfiguredValue;
inherited_value?: boolean;
@@ -750,7 +752,7 @@
* has.
* https://gerrit-review.googlesource.com/Documentation/rest-api-projects.html#label-type-info
*/
-export interface LabelTypeInfo {
+export declare interface LabelTypeInfo {
values: LabelTypeInfoValues;
default_value: number;
}
@@ -765,7 +767,7 @@
* size limit of a project.
* https://gerrit-review.googlesource.com/Documentation/rest-api-projects.html#max-object-size-limit-info
*/
-export interface MaxObjectSizeLimitInfo {
+export declare interface MaxObjectSizeLimitInfo {
value?: string;
configured_value?: string;
summary?: string;
@@ -793,7 +795,7 @@
* plugins.
* https://gerrit-review.googlesource.com/Documentation/rest-api-config.html#plugin-config-info
*/
-export interface PluginConfigInfo {
+export declare interface PluginConfigInfo {
has_avatars: boolean;
// Exists in Java class, but not mentioned in docs.
js_resource_paths: string[];
@@ -828,7 +830,7 @@
* The ProjectInfo entity contains information about a project
* https://gerrit-review.googlesource.com/Documentation/rest-api-projects.html#project-info
*/
-export interface ProjectInfo {
+export declare interface ProjectInfo {
id: UrlEncodedRepoName;
// name is not set if returned in a map where the project name is used as
// map key
@@ -845,7 +847,7 @@
web_links?: WebLinkInfo[];
}
-export interface ProjectInfoWithName extends ProjectInfo {
+export declare interface ProjectInfoWithName extends ProjectInfo {
name: RepoName;
}
@@ -891,7 +893,7 @@
* git-receive-pack behavior on the server.
* https://gerrit-review.googlesource.com/Documentation/rest-api-config.html#receive-info
*/
-export interface ReceiveInfo {
+export declare interface ReceiveInfo {
enable_signed_push?: string;
}
@@ -964,7 +966,7 @@
* Gerrit server.
* https://gerrit-review.googlesource.com/Documentation/rest-api-config.html#server-info
*/
-export interface ServerInfo {
+export declare interface ServerInfo {
accounts: AccountsConfigInfo;
auth: AuthInfo;
change: ChangeConfigInfo;
@@ -999,7 +1001,7 @@
* project inheritance.
* https://gerrit-review.googlesource.com/Documentation/rest-api-projects.html#submit-type-info
*/
-export interface SubmitTypeInfo {
+export declare interface SubmitTypeInfo {
value: Exclude<SubmitType, SubmitType.INHERIT>;
configured_value: SubmitType;
inherited_value: Exclude<SubmitType, SubmitType.INHERIT>;
@@ -1010,7 +1012,7 @@
* the suggest section.
* https://gerrit-review.googlesource.com/Documentation/rest-api-config.html#suggest-info
*/
-export interface SuggestInfo {
+export declare interface SuggestInfo {
from: number;
}
@@ -1038,7 +1040,7 @@
* from the user section.
* https://gerrit-review.googlesource.com/Documentation/rest-api-config.html#user-config-info
*/
-export interface UserConfigInfo {
+export declare interface UserConfigInfo {
anonymous_coward_name: string;
}
diff --git a/polygerrit-ui/app/api/styles.ts b/polygerrit-ui/app/api/styles.ts
index ded3beb..55ac2cc 100644
--- a/polygerrit-ui/app/api/styles.ts
+++ b/polygerrit-ui/app/api/styles.ts
@@ -29,14 +29,15 @@
*/
/** Lit plugins can cast Style to CSSResult. */
-export interface Style {
+export declare interface Style {
toString(): string;
}
-export interface Styles {
+export declare interface Styles {
font: Style;
form: Style;
menuPage: Style;
+ spinner: Style;
subPage: Style;
table: Style;
}
diff --git a/polygerrit-ui/app/compile_generated_templates.sh b/polygerrit-ui/app/compile_generated_templates.sh
new file mode 100755
index 0000000..68bf485
--- /dev/null
+++ b/polygerrit-ui/app/compile_generated_templates.sh
@@ -0,0 +1 @@
+$1 --project $2 --baseUrl ./external/ui_npm/node_modules/ --rootDir null
diff --git a/polygerrit-ui/app/constants/constants.ts b/polygerrit-ui/app/constants/constants.ts
index 8f94b38..645e770 100644
--- a/polygerrit-ui/app/constants/constants.ts
+++ b/polygerrit-ui/app/constants/constants.ts
@@ -306,3 +306,5 @@
}
export const RELOAD_DASHBOARD_INTERVAL_MS = 10 * 1000;
+
+export const SHOWN_ITEMS_COUNT = 25;
diff --git a/polygerrit-ui/app/elements/admin/gr-access-section/gr-access-section.ts b/polygerrit-ui/app/elements/admin/gr-access-section/gr-access-section.ts
index 6efaf0c..2328a05 100644
--- a/polygerrit-ui/app/elements/admin/gr-access-section/gr-access-section.ts
+++ b/polygerrit-ui/app/elements/admin/gr-access-section/gr-access-section.ts
@@ -115,7 +115,7 @@
_updateSection(section: PermissionAccessSection) {
this._permissions = toSortedPermissionsArray(section.value.permissions);
- this._originalId = section.id as GitRef;
+ this._originalId = section.id;
}
_handleAccessSaved() {
@@ -170,7 +170,9 @@
_computePermissions(
name: string,
capabilities?: CapabilityInfoMap,
- labels?: LabelNameToLabelTypeInfoMap
+ labels?: LabelNameToLabelTypeInfoMap,
+ // This is just for triggering re-computation. We don't use the value.
+ _?: unknown
) {
let allPermissions;
const section = this.section;
@@ -227,10 +229,10 @@
_computePermissionName(
name: string,
permission: PermissionArrayItem<EditablePermissionInfo>,
- capabilities: CapabilityInfoMap
- ) {
+ capabilities?: CapabilityInfoMap
+ ): string | undefined {
if (name === GLOBAL_NAME) {
- return capabilities[permission.id].name;
+ return capabilities?.[permission.id].name;
} else if (AccessPermissions[permission.id]) {
return AccessPermissions[permission.id].name;
} else if (permission.value.label) {
@@ -313,7 +315,7 @@
if (
editing &&
this.section &&
- this._isEditEnabled(canUpload, ownerOf, this.section.id as GitRef)
+ this._isEditEnabled(canUpload, ownerOf, this.section.id)
) {
classList.push('editing');
}
@@ -331,7 +333,7 @@
}
_handleAddPermission() {
- const value = this.$.permissionSelect.value;
+ const value = this.$.permissionSelect.value as GitRef;
const permission: PermissionArrayItem<EditablePermissionInfo> = {
id: value,
value: {rules: {}, added: true},
diff --git a/polygerrit-ui/app/elements/admin/gr-admin-group-list/gr-admin-group-list.ts b/polygerrit-ui/app/elements/admin/gr-admin-group-list/gr-admin-group-list.ts
index cdd2913..ea70d7e 100644
--- a/polygerrit-ui/app/elements/admin/gr-admin-group-list/gr-admin-group-list.ts
+++ b/polygerrit-ui/app/elements/admin/gr-admin-group-list/gr-admin-group-list.ts
@@ -23,7 +23,6 @@
import '../gr-create-group-dialog/gr-create-group-dialog';
import {PolymerElement} from '@polymer/polymer/polymer-element';
import {htmlTemplate} from './gr-admin-group-list_html';
-import {ListViewMixin} from '../../../mixins/gr-list-view-mixin/gr-list-view-mixin';
import {GerritNav} from '../../core/gr-navigation/gr-navigation';
import {customElement, property, observe, computed} from '@polymer/decorators';
import {AppElementAdminParams} from '../../gr-app-types';
@@ -32,6 +31,7 @@
import {GrCreateGroupDialog} from '../gr-create-group-dialog/gr-create-group-dialog';
import {fireTitleChange} from '../../../utils/event-util';
import {appContext} from '../../../services/app-context';
+import {SHOWN_ITEMS_COUNT} from '../../../constants/constants';
declare global {
interface HTMLElementTagNameMap {
@@ -46,11 +46,8 @@
};
}
-// This avoids JSC_DYNAMIC_EXTENDS_WITHOUT_JSDOC closure compiler error.
-const base = ListViewMixin(PolymerElement);
-
@customElement('gr-admin-group-list')
-export class GrAdminGroupList extends base {
+export class GrAdminGroupList extends PolymerElement {
static get template() {
return htmlTemplate;
}
@@ -62,7 +59,7 @@
* Offset of currently visible query results.
*/
@property({type: Number})
- _offset?: number;
+ _offset = 0;
@property({type: String})
readonly _path = '/admin/groups';
@@ -82,7 +79,7 @@
* */
@computed('_groups')
get _shownGroups() {
- return this.computeShownItems(this._groups);
+ return this._groups.slice(0, SHOWN_ITEMS_COUNT);
}
@property({type: Number})
@@ -106,8 +103,8 @@
@observe('params')
_paramsChanged(params: AppElementAdminParams) {
this._loading = true;
- this._filter = this.getFilterValue(params);
- this._offset = this.getOffsetValue(params);
+ this._filter = params?.filter ?? '';
+ this._offset = Number(params?.offset ?? 0);
return this._getGroups(this._filter, this._groupsPerPage, this._offset);
}
@@ -184,4 +181,8 @@
_visibleToAll(item: GroupInfo) {
return item.options?.visible_to_all === true ? 'Y' : 'N';
}
+
+ computeLoadingClass(loading: boolean) {
+ return loading ? 'loading' : '';
+ }
}
diff --git a/polygerrit-ui/app/elements/admin/gr-admin-view/gr-admin-view_html.ts b/polygerrit-ui/app/elements/admin/gr-admin-view/gr-admin-view_html.ts
index 70e146f..a3afc5c 100644
--- a/polygerrit-ui/app/elements/admin/gr-admin-view/gr-admin-view_html.ts
+++ b/polygerrit-ui/app/elements/admin/gr-admin-view/gr-admin-view_html.ts
@@ -24,11 +24,6 @@
/* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
</style>
<style include="gr-page-nav-styles">
- gr-dropdown-list {
- --trigger-style: {
- text-transform: none;
- }
- }
.breadcrumbText {
/* Same as dropdown trigger so chevron spacing is consistent. */
padding: 5px 4px;
diff --git a/polygerrit-ui/app/elements/admin/gr-confirm-delete-item-dialog/gr-confirm-delete-item-dialog.ts b/polygerrit-ui/app/elements/admin/gr-confirm-delete-item-dialog/gr-confirm-delete-item-dialog.ts
index 5a48399..04d3198 100644
--- a/polygerrit-ui/app/elements/admin/gr-confirm-delete-item-dialog/gr-confirm-delete-item-dialog.ts
+++ b/polygerrit-ui/app/elements/admin/gr-confirm-delete-item-dialog/gr-confirm-delete-item-dialog.ts
@@ -45,7 +45,7 @@
@property({type: String})
itemTypeName?: string;
- static get styles() {
+ static override get styles() {
return [
sharedStyles,
css`
@@ -57,7 +57,7 @@
];
}
- render() {
+ override render() {
const item = this.item ?? 'UNKNOWN ITEM';
const itemTypeName = this.itemTypeName ?? 'UNKNOWN ITEM TYPE';
return html` <gr-dialog
diff --git a/polygerrit-ui/app/elements/admin/gr-create-change-dialog/gr-create-change-dialog.ts b/polygerrit-ui/app/elements/admin/gr-create-change-dialog/gr-create-change-dialog.ts
index 63b10ec..15f6f4b 100644
--- a/polygerrit-ui/app/elements/admin/gr-create-change-dialog/gr-create-change-dialog.ts
+++ b/polygerrit-ui/app/elements/admin/gr-create-change-dialog/gr-create-change-dialog.ts
@@ -32,7 +32,7 @@
InheritedBooleanInfo,
} from '../../../types/common';
import {InheritedBooleanInfoConfiguredValue} from '../../../constants/constants';
-import {GrAutocomplete} from '../../shared/gr-autocomplete/gr-autocomplete';
+import {GrTypedAutocomplete} from '../../shared/gr-autocomplete/gr-autocomplete';
import {IronAutogrowTextareaElement} from '@polymer/iron-autogrow-textarea/iron-autogrow-textarea';
import {appContext} from '../../../services/app-context';
import {Subject} from 'rxjs';
@@ -41,6 +41,7 @@
serverConfig$,
} from '../../../services/config/config-model';
import {takeUntil} from 'rxjs/operators';
+import {IronInputElement} from '@polymer/iron-input/iron-input';
const SUGGESTIONS_LIMIT = 15;
const REF_PREFIX = 'refs/heads/';
@@ -48,8 +49,8 @@
export interface GrCreateChangeDialog {
$: {
privateChangeCheckBox: HTMLInputElement;
- branchInput: GrAutocomplete;
- tagNameInput: HTMLInputElement;
+ branchInput: GrTypedAutocomplete<BranchName>;
+ tagNameInput: IronInputElement;
messageInput: IronAutogrowTextareaElement;
};
}
@@ -63,19 +64,19 @@
repoName?: RepoName;
@property({type: String})
- branch?: BranchName;
+ branch = '' as BranchName;
@property({type: Object})
_repoConfig?: ConfigInfo;
@property({type: String})
- subject?: string;
+ subject = '';
@property({type: String})
topic?: string;
@property({type: Object})
- _query?: (input: string) => Promise<{name: string}[]>;
+ _query?: (input: string) => Promise<{name: BranchName}[]>;
@property({type: String})
baseChange?: ChangeId;
@@ -90,7 +91,7 @@
canCreate = false;
@property({type: Boolean})
- _privateChangesEnabled?: boolean;
+ _privateChangesEnabled = false;
restApiService = appContext.restApiService;
@@ -120,7 +121,7 @@
super.disconnectedCallback();
}
- _computeBranchClass(baseChange: boolean) {
+ _computeBranchClass(baseChange?: ChangeId) {
return baseChange ? 'hide' : '';
}
@@ -165,19 +166,19 @@
.getRepoBranches(input, this.repoName, SUGGESTIONS_LIMIT)
.then(response => {
if (!response) return [];
- const branches = [];
+ const branches: Array<{name: BranchName}> = [];
for (const branchInfo of response) {
let name: string = branchInfo.ref;
if (name.startsWith('refs/heads/')) {
name = name.substring('refs/heads/'.length);
}
- branches.push({name});
+ branches.push({name: name as BranchName});
}
return branches;
});
}
- _formatBooleanString(config: InheritedBooleanInfo) {
+ _formatBooleanString(config?: InheritedBooleanInfo) {
if (
config &&
config.configured_value === InheritedBooleanInfoConfiguredValue.TRUE
diff --git a/polygerrit-ui/app/elements/admin/gr-create-change-dialog/gr-create-change-dialog_test.ts b/polygerrit-ui/app/elements/admin/gr-create-change-dialog/gr-create-change-dialog_test.ts
index a9de24a..9ed5d81 100644
--- a/polygerrit-ui/app/elements/admin/gr-create-change-dialog/gr-create-change-dialog_test.ts
+++ b/polygerrit-ui/app/elements/admin/gr-create-change-dialog/gr-create-change-dialog_test.ts
@@ -20,7 +20,11 @@
import {GrCreateChangeDialog} from './gr-create-change-dialog';
import {BranchName, GitRef, RepoName} from '../../../types/common';
import {InheritedBooleanInfoConfiguredValue} from '../../../constants/constants';
-import {createChange, createConfig} from '../../../test/test-data-generators';
+import {
+ createChange,
+ createConfig,
+ TEST_CHANGE_ID,
+} from '../../../test/test-data-generators';
import {stubRestApi} from '../../../test/test-utils';
const basicFixture = fixtureFromElement('gr-create-change-dialog');
@@ -130,8 +134,8 @@
});
test('_computeBranchClass', () => {
- assert.equal(element._computeBranchClass(true), 'hide');
- assert.equal(element._computeBranchClass(false), '');
+ assert.equal(element._computeBranchClass(TEST_CHANGE_ID), 'hide');
+ assert.equal(element._computeBranchClass(undefined), '');
});
test('_computePrivateSectionClass', () => {
diff --git a/polygerrit-ui/app/elements/admin/gr-group-audit-log/gr-group-audit-log.ts b/polygerrit-ui/app/elements/admin/gr-group-audit-log/gr-group-audit-log.ts
index d40b810..6605350 100644
--- a/polygerrit-ui/app/elements/admin/gr-group-audit-log/gr-group-audit-log.ts
+++ b/polygerrit-ui/app/elements/admin/gr-group-audit-log/gr-group-audit-log.ts
@@ -17,11 +17,9 @@
import '../../../styles/gr-table-styles';
import '../../../styles/shared-styles';
-import '../../shared/gr-date-formatter/gr-date-formatter';
import '../../shared/gr-account-link/gr-account-link';
import {PolymerElement} from '@polymer/polymer/polymer-element';
import {htmlTemplate} from './gr-group-audit-log_html';
-import {ListViewMixin} from '../../../mixins/gr-list-view-mixin/gr-list-view-mixin';
import {GerritNav} from '../../core/gr-navigation/gr-navigation';
import {customElement, property} from '@polymer/decorators';
import {
@@ -36,11 +34,8 @@
import {appContext} from '../../../services/app-context';
import {ErrorCallback} from '../../../api/rest';
-// This avoids JSC_DYNAMIC_EXTENDS_WITHOUT_JSDOC closure compiler error.
-const base = ListViewMixin(PolymerElement);
-
@customElement('gr-group-audit-log')
-export class GrGroupAuditLog extends base {
+export class GrGroupAuditLog extends PolymerElement {
static get template() {
return htmlTemplate;
}
@@ -130,6 +125,10 @@
return '';
}
+
+ computeLoadingClass(loading: boolean) {
+ return loading ? 'loading' : '';
+ }
}
declare global {
diff --git a/polygerrit-ui/app/elements/admin/gr-group-audit-log/gr-group-audit-log_html.ts b/polygerrit-ui/app/elements/admin/gr-group-audit-log/gr-group-audit-log_html.ts
index 40c2f30..828aa55 100644
--- a/polygerrit-ui/app/elements/admin/gr-group-audit-log/gr-group-audit-log_html.ts
+++ b/polygerrit-ui/app/elements/admin/gr-group-audit-log/gr-group-audit-log_html.ts
@@ -43,7 +43,7 @@
<template is="dom-repeat" items="[[_auditLog]]">
<tr class="table">
<td class="date">
- <gr-date-formatter has-tooltip="" date-str="[[item.date]]">
+ <gr-date-formatter withTooltip date-str="[[item.date]]">
</gr-date-formatter>
</td>
<td class="type">[[itemType(item.type)]]</td>
diff --git a/polygerrit-ui/app/elements/admin/gr-group-members/gr-group-members_test.js b/polygerrit-ui/app/elements/admin/gr-group-members/gr-group-members_test.js
index d26d14c..b91b04b 100644
--- a/polygerrit-ui/app/elements/admin/gr-group-members/gr-group-members_test.js
+++ b/polygerrit-ui/app/elements/admin/gr-group-members/gr-group-members_test.js
@@ -140,7 +140,7 @@
'https://test/site/group/url');
});
- test('save members correctly', () => {
+ test('save members correctly', async () => {
element._groupOwner = true;
const memberName = 'test-admin';
@@ -155,6 +155,7 @@
element.$.groupMemberSearchInput.text = memberName;
element.$.groupMemberSearchInput.value = 1234;
+ await flush();
assert.isFalse(button.hasAttribute('disabled'));
return element._handleSavingGroupMember().then(() => {
@@ -165,7 +166,7 @@
});
});
- test('save included groups correctly', () => {
+ test('save included groups correctly', async () => {
element._groupOwner = true;
const includedGroupName = 'testName';
@@ -179,7 +180,7 @@
element.$.includedGroupSearchInput.text = includedGroupName;
element.$.includedGroupSearchInput.value = 'testId';
-
+ await flush();
assert.isFalse(button.hasAttribute('disabled'));
return element._handleSavingIncludedGroups().then(() => {
diff --git a/polygerrit-ui/app/elements/admin/gr-group/gr-group_test.js b/polygerrit-ui/app/elements/admin/gr-group/gr-group_test.js
index e492a15..e390ac5 100644
--- a/polygerrit-ui/app/elements/admin/gr-group/gr-group_test.js
+++ b/polygerrit-ui/app/elements/admin/gr-group/gr-group_test.js
@@ -94,6 +94,7 @@
element.$.groupNameInput.text = groupName2;
+ await flush();
assert.isFalse(button.hasAttribute('disabled'));
assert.isTrue(element.$.groupName.classList.contains('edited'));
@@ -122,6 +123,7 @@
element.$.groupOwnerInput.text = 'testId2';
+ await flush();
assert.isFalse(button.hasAttribute('disabled'));
assert.isTrue(element.$.groupOwner.classList.contains('edited'));
diff --git a/polygerrit-ui/app/elements/admin/gr-plugin-config-array-editor/gr-plugin-config-array-editor.ts b/polygerrit-ui/app/elements/admin/gr-plugin-config-array-editor/gr-plugin-config-array-editor.ts
index 9eb9b07..2d3b5c3 100644
--- a/polygerrit-ui/app/elements/admin/gr-plugin-config-array-editor/gr-plugin-config-array-editor.ts
+++ b/polygerrit-ui/app/elements/admin/gr-plugin-config-array-editor/gr-plugin-config-array-editor.ts
@@ -28,7 +28,6 @@
PluginConfigOptionsChangedEventDetail,
ArrayPluginOption,
} from '../gr-repo-plugin-config/gr-repo-plugin-config-types';
-import {KeydownEvent} from '../../../types/events';
declare global {
interface HTMLElementTagNameMap {
@@ -75,7 +74,7 @@
this._handleAdd();
}
- _handleInputKeydown(e: KeydownEvent) {
+ _handleInputKeydown(e: KeyboardEvent) {
// Enter.
if (e.keyCode === 13) {
e.preventDefault();
diff --git a/polygerrit-ui/app/elements/admin/gr-plugin-list/gr-plugin-list.ts b/polygerrit-ui/app/elements/admin/gr-plugin-list/gr-plugin-list.ts
index 13bd79d..9f51688 100644
--- a/polygerrit-ui/app/elements/admin/gr-plugin-list/gr-plugin-list.ts
+++ b/polygerrit-ui/app/elements/admin/gr-plugin-list/gr-plugin-list.ts
@@ -19,25 +19,21 @@
import '../../shared/gr-list-view/gr-list-view';
import {PolymerElement} from '@polymer/polymer/polymer-element';
import {htmlTemplate} from './gr-plugin-list_html';
-import {
- ListViewMixin,
- ListViewParams,
-} from '../../../mixins/gr-list-view-mixin/gr-list-view-mixin';
import {customElement, property} from '@polymer/decorators';
import {PluginInfo} from '../../../types/common';
import {firePageError, fireTitleChange} from '../../../utils/event-util';
import {appContext} from '../../../services/app-context';
import {ErrorCallback} from '../../../api/rest';
+import {encodeURL, getBaseUrl} from '../../../utils/url-util';
+import {SHOWN_ITEMS_COUNT} from '../../../constants/constants';
+import {ListViewParams} from '../../gr-app-types';
interface PluginInfoWithName extends PluginInfo {
name: string;
}
-// This avoids JSC_DYNAMIC_EXTENDS_WITHOUT_JSDOC closure compiler error.
-const base = ListViewMixin(PolymerElement);
-
@customElement('gr-plugin-list')
-export class GrPluginList extends base {
+export class GrPluginList extends PolymerElement {
static get template() {
return htmlTemplate;
}
@@ -85,8 +81,8 @@
_paramsChanged(params: ListViewParams) {
this._loading = true;
- this._filter = this.getFilterValue(params);
- this._offset = this.getOffsetValue(params);
+ this._filter = params?.filter ?? '';
+ this._offset = Number(params?.offset ?? 0);
return this._getPlugins(this._filter, this._pluginsPerPage, this._offset);
}
@@ -114,7 +110,15 @@
}
_computePluginUrl(id: string) {
- return this.getUrl('/', id);
+ return getBaseUrl() + '/' + encodeURL(id, true);
+ }
+
+ computeLoadingClass(loading: boolean) {
+ return loading ? 'loading' : '';
+ }
+
+ computeShownItems(plugins: PluginInfoWithName[]) {
+ return plugins.slice(0, SHOWN_ITEMS_COUNT);
}
}
diff --git a/polygerrit-ui/app/elements/admin/gr-repo-dashboards/gr-repo-dashboards.ts b/polygerrit-ui/app/elements/admin/gr-repo-dashboards/gr-repo-dashboards.ts
index fc7ef4b..7800653 100644
--- a/polygerrit-ui/app/elements/admin/gr-repo-dashboards/gr-repo-dashboards.ts
+++ b/polygerrit-ui/app/elements/admin/gr-repo-dashboards/gr-repo-dashboards.ts
@@ -43,7 +43,7 @@
private readonly restApiService = appContext.restApiService;
- static get styles() {
+ static override get styles() {
return [
sharedStyles,
tableStyles,
@@ -63,7 +63,7 @@
];
}
- render() {
+ override render() {
return html` <table
id="list"
class="genericList ${this._computeLoadingClass(this._loading)}"
@@ -114,7 +114,7 @@
</table>`;
}
- updated(changedProperties: PropertyValues) {
+ override updated(changedProperties: PropertyValues) {
if (changedProperties.has('repo')) {
this.repoChanged();
}
diff --git a/polygerrit-ui/app/elements/admin/gr-repo-detail-list/gr-repo-detail-list.ts b/polygerrit-ui/app/elements/admin/gr-repo-detail-list/gr-repo-detail-list.ts
index 3693845..052e07a 100644
--- a/polygerrit-ui/app/elements/admin/gr-repo-detail-list/gr-repo-detail-list.ts
+++ b/polygerrit-ui/app/elements/admin/gr-repo-detail-list/gr-repo-detail-list.ts
@@ -30,7 +30,6 @@
import {flush} from '@polymer/polymer/lib/legacy/polymer.dom';
import {PolymerElement} from '@polymer/polymer/polymer-element';
import {htmlTemplate} from './gr-repo-detail-list_html';
-import {ListViewMixin} from '../../../mixins/gr-list-view-mixin/gr-list-view-mixin';
import {encodeURL} from '../../../utils/url-util';
import {customElement, property} from '@polymer/decorators';
import {GrOverlay} from '../../shared/gr-overlay/gr-overlay';
@@ -49,6 +48,7 @@
import {firePageError} from '../../../utils/event-util';
import {appContext} from '../../../services/app-context';
import {ErrorCallback} from '../../../api/rest';
+import {SHOWN_ITEMS_COUNT} from '../../../constants/constants';
const PGP_START = '-----BEGIN PGP SIGNATURE-----';
@@ -60,11 +60,8 @@
};
}
-// This avoids JSC_DYNAMIC_EXTENDS_WITHOUT_JSDOC closure compiler error.
-const base = ListViewMixin(PolymerElement);
-
@customElement('gr-repo-detail-list')
-export class GrRepoDetailList extends base {
+export class GrRepoDetailList extends PolymerElement {
static get template() {
return htmlTemplate;
}
@@ -85,7 +82,7 @@
_loggedIn = false;
@property({type: Number})
- _offset?: number;
+ _offset = 0;
@property({type: String})
_repo?: RepoName;
@@ -155,8 +152,8 @@
this.detailType = params.detail;
- this._filter = this.getFilterValue(params);
- this._offset = this.getOffsetValue(params);
+ this._filter = params?.filter ?? '';
+ this._offset = Number(params?.offset ?? 0);
if (!this.detailType)
return Promise.reject(new Error('undefined detailType'));
@@ -395,6 +392,14 @@
_computeHideTagger(tagger?: GitPersonInfo) {
return tagger ? '' : 'hide';
}
+
+ computeLoadingClass(loading: boolean) {
+ return loading ? 'loading' : '';
+ }
+
+ computeShownItems(items: BranchInfo[] | TagInfo[]) {
+ return items.slice(0, SHOWN_ITEMS_COUNT);
+ }
}
declare global {
diff --git a/polygerrit-ui/app/elements/admin/gr-repo-detail-list/gr-repo-detail-list_html.ts b/polygerrit-ui/app/elements/admin/gr-repo-detail-list/gr-repo-detail-list_html.ts
index 4f66f0d..429a6d6 100644
--- a/polygerrit-ui/app/elements/admin/gr-repo-detail-list/gr-repo-detail-list_html.ts
+++ b/polygerrit-ui/app/elements/admin/gr-repo-detail-list/gr-repo-detail-list_html.ts
@@ -145,10 +145,7 @@
<td class$="tagger [[_hideIfBranch(detailType)]]">
<div class$="tagger [[_computeHideTagger(item.tagger)]]">
<gr-account-link account="[[item.tagger]]"> </gr-account-link>
- (<gr-date-formatter
- has-tooltip=""
- date-str="[[item.tagger.date]]"
- >
+ (<gr-date-formatter withTooltip date-str="[[item.tagger.date]]">
</gr-date-formatter
>)
</div>
diff --git a/polygerrit-ui/app/elements/admin/gr-repo-list/gr-repo-list.ts b/polygerrit-ui/app/elements/admin/gr-repo-list/gr-repo-list.ts
index e87bb0d..adcfb64 100644
--- a/polygerrit-ui/app/elements/admin/gr-repo-list/gr-repo-list.ts
+++ b/polygerrit-ui/app/elements/admin/gr-repo-list/gr-repo-list.ts
@@ -22,16 +22,16 @@
import '../gr-create-repo-dialog/gr-create-repo-dialog';
import {PolymerElement} from '@polymer/polymer/polymer-element';
import {htmlTemplate} from './gr-repo-list_html';
-import {ListViewMixin} from '../../../mixins/gr-list-view-mixin/gr-list-view-mixin';
import {GerritNav} from '../../core/gr-navigation/gr-navigation';
import {customElement, property, observe, computed} from '@polymer/decorators';
import {AppElementAdminParams} from '../../gr-app-types';
import {GrOverlay} from '../../shared/gr-overlay/gr-overlay';
import {RepoName, ProjectInfoWithName} from '../../../types/common';
import {GrCreateRepoDialog} from '../gr-create-repo-dialog/gr-create-repo-dialog';
-import {ProjectState} from '../../../constants/constants';
+import {ProjectState, SHOWN_ITEMS_COUNT} from '../../../constants/constants';
import {fireTitleChange} from '../../../utils/event-util';
import {appContext} from '../../../services/app-context';
+import {encodeURL, getBaseUrl} from '../../../utils/url-util';
declare global {
interface HTMLElementTagNameMap {
@@ -46,11 +46,8 @@
};
}
-// This avoids JSC_DYNAMIC_EXTENDS_WITHOUT_JSDOC closure compiler error.
-const base = ListViewMixin(PolymerElement);
-
@customElement('gr-repo-list')
-export class GrRepoList extends base {
+export class GrRepoList extends PolymerElement {
static get template() {
return htmlTemplate;
}
@@ -59,7 +56,7 @@
params?: AppElementAdminParams;
@property({type: Number})
- _offset?: number;
+ _offset = 0;
@property({type: String})
readonly _path = '/admin/repos';
@@ -84,7 +81,7 @@
@computed('_repos')
get _shownRepos() {
- return this.computeShownItems(this._repos);
+ return this._repos.slice(0, SHOWN_ITEMS_COUNT);
}
private readonly restApiService = appContext.restApiService;
@@ -99,8 +96,8 @@
@observe('params')
_paramsChanged(params: AppElementAdminParams) {
this._loading = true;
- this._filter = this.getFilterValue(params);
- this._offset = this.getOffsetValue(params);
+ this._filter = params?.filter ?? '';
+ this._offset = Number(params?.offset ?? 0);
return this._getRepos(this._filter, this._reposPerPage, this._offset);
}
@@ -115,7 +112,7 @@
}
_computeRepoUrl(name: string) {
- return this.getUrl(this._path + '/', name);
+ return getBaseUrl() + this._path + '/' + encodeURL(name, true);
}
_computeChangesLink(name: string) {
@@ -185,4 +182,8 @@
const webLinks = repo.web_links;
return webLinks.length ? webLinks : null;
}
+
+ computeLoadingClass(loading: boolean) {
+ return loading ? 'loading' : '';
+ }
}
diff --git a/polygerrit-ui/app/elements/admin/gr-repo-plugin-config/gr-repo-plugin-config.ts b/polygerrit-ui/app/elements/admin/gr-repo-plugin-config/gr-repo-plugin-config.ts
index 02339cf..32812dd 100644
--- a/polygerrit-ui/app/elements/admin/gr-repo-plugin-config/gr-repo-plugin-config.ts
+++ b/polygerrit-ui/app/elements/admin/gr-repo-plugin-config/gr-repo-plugin-config.ts
@@ -119,15 +119,7 @@
private renderOption(option: PluginOption) {
return html`
<section class="section ${option.info.type}">
- <span class="title">
- <gr-tooltip-content
- has-tooltip="${option.info.description}"
- show-icon="${option.info.description}"
- title="${option.info.description}"
- >
- <span>${option.info.display_name}</span>
- </gr-tooltip-content>
- </span>
+ <span class="title"> ${this.renderOptionTitle(option)} </span>
<span class="value">
${this.renderOptionDetail(option)} ${this.renderInherited(option)}
</span>
@@ -135,12 +127,24 @@
`;
}
+ private renderOptionTitle(option: PluginOption) {
+ const titleName = html`<span>${option.info.display_name}</span>`;
+ if (!option.info.description) return titleName;
+ return html` <gr-tooltip-content
+ has-tooltip
+ show-icon
+ title="${option.info.description}"
+ >
+ ${titleName}
+ </gr-tooltip-content>`;
+ }
+
private renderOptionDetail(option: PluginOption) {
if (option.info.type === ConfigParameterInfoType.ARRAY) {
return html`
<gr-plugin-config-array-editor
@plugin-config-option-changed=${this._handleArrayChange}
- plugin-option="${option}"
+ .pluginOption="${option}"
></gr-plugin-config-array-editor>
`;
} else if (option.info.type === ConfigParameterInfoType.BOOLEAN) {
@@ -155,7 +159,10 @@
`;
} else if (option.info.type === ConfigParameterInfoType.LIST) {
return html`
- <gr-select value=${option.info.value} @change=${this._handleListChange}>
+ <gr-select
+ .bindValue=${option.info.value}
+ @change=${this._handleListChange}
+ >
<select
data-option-key=${option._key}
?disabled=${!option.info.editable}
@@ -173,14 +180,12 @@
) {
return html`
<iron-input
- value=${option.info.value}
@input=${this._handleStringChange}
data-option-key="${option._key}"
- ?disabled=${!option.info.editable}
>
<input
is="iron-input"
- value="${option.info.value}"
+ .value="${option.info.value ?? ''}"
@input=${this._handleStringChange}
data-option-key="${option._key}"
?disabled=${!option.info.editable}
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item.ts b/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item.ts
index 313b91f..c476d2d 100644
--- a/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item.ts
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item.ts
@@ -28,7 +28,6 @@
import '../../plugins/gr-endpoint-param/gr-endpoint-param';
import {PolymerElement} from '@polymer/polymer/polymer-element';
import {htmlTemplate} from './gr-change-list-item_html';
-import {ChangeTableMixin} from '../../../mixins/gr-change-table-mixin/gr-change-table-mixin';
import {GerritNav} from '../../core/gr-navigation/gr-navigation';
import {getDisplayName} from '../../../utils/display-name-util';
import {getPluginEndpoints} from '../../shared/gr-js-api-interface/gr-plugin-endpoints';
@@ -76,11 +75,8 @@
// How many reviewers should be shown with an account-label?
const PRIMARY_REVIEWERS_COUNT = 2;
-// This avoids JSC_DYNAMIC_EXTENDS_WITHOUT_JSDOC closure compiler error.
-const base = ChangeTableMixin(PolymerElement);
-
@customElement('gr-change-list-item')
-export class GrChangeListItem extends base {
+export class GrChangeListItem extends PolymerElement {
static get template() {
return htmlTemplate;
}
@@ -288,7 +284,7 @@
* @param truncate whether or not the project name should be
* truncated. If this value is truthy, the name will be truncated.
*/
- _computeRepoDisplay(change: ChangeInfo | undefined, truncate: boolean) {
+ _computeRepoDisplay(change?: ChangeInfo) {
if (!change?.project) {
return '';
}
@@ -296,7 +292,19 @@
if (change.internalHost) {
str += change.internalHost + '/';
}
- str += truncate ? truncatePath(change.project, 2) : change.project;
+ str += change.project;
+ return str;
+ }
+
+ _computeTruncatedRepoDisplay(change?: ChangeInfo) {
+ if (!change?.project) {
+ return '';
+ }
+ let str = '';
+ if (change.internalHost) {
+ str += change.internalHost + '/';
+ }
+ str += truncatePath(change.project, 2);
return str;
}
@@ -354,10 +362,7 @@
return this._computeAdditionalReviewers(change).length;
}
- _computeAdditionalReviewersTitle(
- change: ChangeInfo | undefined,
- config: ServerInfo
- ) {
+ _computeAdditionalReviewersTitle(change?: ChangeInfo, config?: ServerInfo) {
if (!change || !config) return '';
return this._computeAdditionalReviewers(change)
.map(user => getDisplayName(config, user, true))
@@ -393,13 +398,20 @@
}
_computeWaiting(
- account?: AccountInfo,
- change?: ChangeInfo
+ account?: AccountInfo | null,
+ change?: ChangeInfo | null
): Timestamp | undefined {
if (!account?._account_id || !change?.attention_set) return undefined;
return change?.attention_set[account._account_id]?.last_update;
}
+ _computeIsColumnHidden(columnToCheck?: string, columnsToDisplay?: string[]) {
+ if (!columnsToDisplay || !columnToCheck) {
+ return false;
+ }
+ return !columnsToDisplay.includes(columnToCheck);
+ }
+
toggleReviewed() {
if (!this.change) return;
const newVal = !this.change?.reviewed;
@@ -417,6 +429,11 @@
);
}
+ _formatDate(date: Timestamp | undefined): string | undefined {
+ if (!date) return undefined;
+ return date.toString();
+ }
+
_handleChangeClick() {
// Don't prevent the default and neither stop bubbling. We just want to
// report the click, but then let the browser handle the click on the link.
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item_html.ts b/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item_html.ts
index 8ee6a3f..a0aa962 100644
--- a/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item_html.ts
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item_html.ts
@@ -127,7 +127,7 @@
</td>
<td
class="cell subject"
- hidden$="[[isColumnHidden('Subject', visibleChangeTableColumns)]]"
+ hidden$="[[_computeIsColumnHidden('Subject', visibleChangeTableColumns)]]"
>
<a
title$="[[change.subject]]"
@@ -143,7 +143,7 @@
</td>
<td
class="cell status"
- hidden$="[[isColumnHidden('Status', visibleChangeTableColumns)]]"
+ hidden$="[[_computeIsColumnHidden('Status', visibleChangeTableColumns)]]"
>
<template is="dom-repeat" items="[[statuses]]" as="status">
<div class="comma">,</div>
@@ -155,7 +155,7 @@
</td>
<td
class="cell owner"
- hidden$="[[isColumnHidden('Owner', visibleChangeTableColumns)]]"
+ hidden$="[[_computeIsColumnHidden('Owner', visibleChangeTableColumns)]]"
>
<gr-account-link
highlightAttention
@@ -165,7 +165,7 @@
</td>
<td
class="cell assignee"
- hidden$="[[isColumnHidden('Assignee', visibleChangeTableColumns)]]"
+ hidden$="[[_computeIsColumnHidden('Assignee', visibleChangeTableColumns)]]"
>
<template is="dom-if" if="[[change.assignee]]">
<gr-account-link
@@ -179,7 +179,7 @@
</td>
<td
class="cell reviewers"
- hidden$="[[isColumnHidden('Reviewers', visibleChangeTableColumns)]]"
+ hidden$="[[_computeIsColumnHidden('Reviewers', visibleChangeTableColumns)]]"
>
<div>
<template
@@ -204,14 +204,14 @@
</template>
<template is="dom-if" if="[[_computeAdditionalReviewersCount(change)]]">
<span title="[[_computeAdditionalReviewersTitle(change, config)]]">
- +[[_computeAdditionalReviewersCount(change, config)]]
+ +[[_computeAdditionalReviewersCount(change)]]
</span>
</template>
</div>
</td>
<td
class="cell comments"
- hidden$="[[isColumnHidden('Comments', visibleChangeTableColumns)]]"
+ hidden$="[[_computeIsColumnHidden('Comments', visibleChangeTableColumns)]]"
>
<iron-icon
hidden$="[[!change.unresolved_comment_count]]"
@@ -221,7 +221,7 @@
</td>
<td
class="cell repo"
- hidden$="[[isColumnHidden('Repo', visibleChangeTableColumns)]]"
+ hidden$="[[_computeIsColumnHidden('Repo', visibleChangeTableColumns)]]"
>
<a class="fullRepo" href$="[[_computeRepoUrl(change)]]">
[[_computeRepoDisplay(change)]]
@@ -231,12 +231,12 @@
href$="[[_computeRepoUrl(change)]]"
title$="[[_computeRepoDisplay(change)]]"
>
- [[_computeRepoDisplay(change, 'true')]]
+ [[_computeTruncatedRepoDisplay(change)]]
</a>
</td>
<td
class="cell branch"
- hidden$="[[isColumnHidden('Branch', visibleChangeTableColumns)]]"
+ hidden$="[[_computeIsColumnHidden('Branch', visibleChangeTableColumns)]]"
>
<a href$="[[_computeRepoBranchURL(change)]]"> [[change.branch]] </a>
<template is="dom-if" if="[[change.topic]]">
@@ -250,38 +250,38 @@
</td>
<td
class="cell updated"
- hidden$="[[isColumnHidden('Updated', visibleChangeTableColumns)]]"
+ hidden$="[[_computeIsColumnHidden('Updated', visibleChangeTableColumns)]]"
>
<gr-date-formatter
- has-tooltip=""
- date-str="[[change.updated]]"
+ withTooltip
+ date-str="[[_formatDate(change.updated)]]"
></gr-date-formatter>
</td>
<td
class="cell submitted"
- hidden$="[[isColumnHidden('Submitted', visibleChangeTableColumns)]]"
+ hidden$="[[_computeIsColumnHidden('Submitted', visibleChangeTableColumns)]]"
>
<gr-date-formatter
- has-tooltip=""
- date-str="[[change.submitted]]"
+ withTooltip
+ date-str="[[_formatDate(change.submitted)]]"
></gr-date-formatter>
</td>
<td
class="cell waiting"
- hidden$="[[isColumnHidden('Waiting', visibleChangeTableColumns)]]"
+ hidden$="[[_computeIsColumnHidden('Waiting', visibleChangeTableColumns)]]"
>
<gr-date-formatter
- has-tooltip=""
- force-relative=""
- relative-option-no-ago=""
+ withTooltip
+ forceRelative
+ relativeOptionNoAgo
date-str="[[_computeWaiting(account, change)]]"
></gr-date-formatter>
</td>
<td
class="cell size"
- hidden$="[[isColumnHidden('Size', visibleChangeTableColumns)]]"
+ hidden$="[[_computeIsColumnHidden('Size', visibleChangeTableColumns)]]"
>
- <gr-tooltip-content has-tooltip="" title="[[_computeSizeTooltip(change)]]">
+ <gr-tooltip-content has-tooltip title="[[_computeSizeTooltip(change)]]">
<template is="dom-if" if="[[_changeSize]]">
<span>[[_changeSize]]</span>
</template>
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item_test.ts b/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item_test.ts
index ac0b929..34cb6eb 100644
--- a/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item_test.ts
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item_test.ts
@@ -29,6 +29,7 @@
TopicName,
} from '../../../types/common';
import {GerritNav} from '../../core/gr-navigation/gr-navigation';
+import {columnNames} from '../gr-change-list/gr-change-list';
import './gr-change-list-item';
import {GrChangeListItem, LabelCategory} from './gr-change-list-item';
@@ -372,7 +373,7 @@
await flush();
- for (const column of element.columnNames) {
+ for (const column of columnNames) {
const elementClass = '.' + column.toLowerCase();
assert.isFalse(
queryAndAssert(element, elementClass).hasAttribute('hidden')
@@ -395,7 +396,7 @@
await flush();
- for (const column of element.columnNames) {
+ for (const column of columnNames) {
const elementClass = '.' + column.toLowerCase();
if (column === 'Repo') {
assert.isTrue(
@@ -565,13 +566,13 @@
});
test('_computeRepoDisplay', () => {
+ assert.equal(element._computeRepoDisplay(change), 'host/a/test/repo');
assert.equal(
- element._computeRepoDisplay(change, false),
- 'host/a/test/repo'
+ element._computeTruncatedRepoDisplay(change),
+ 'host/…/test/repo'
);
- assert.equal(element._computeRepoDisplay(change, true), 'host/…/test/repo');
delete change.internalHost;
- assert.equal(element._computeRepoDisplay(change, false), 'a/test/repo');
- assert.equal(element._computeRepoDisplay(change, true), '…/test/repo');
+ assert.equal(element._computeRepoDisplay(change), 'a/test/repo');
+ assert.equal(element._computeTruncatedRepoDisplay(change), '…/test/repo');
});
});
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list.ts b/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list.ts
index 40674d4..ac44908 100644
--- a/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list.ts
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list.ts
@@ -24,7 +24,6 @@
import {PolymerElement} from '@polymer/polymer/polymer-element';
import {htmlTemplate} from './gr-change-list_html';
import {appContext} from '../../../services/app-context';
-import {ChangeTableMixin} from '../../../mixins/gr-change-table-mixin/gr-change-table-mixin';
import {
KeyboardShortcutMixin,
Shortcut,
@@ -47,9 +46,9 @@
PreferencesInput,
} from '../../../types/common';
import {hasAttention} from '../../../utils/attention-set-util';
-import {CustomKeyboardEvent} from '../../../types/events';
+import {IronKeyboardEvent} from '../../../types/events';
import {fireEvent, fireReload} from '../../../utils/event-util';
-import {isShiftPressed} from '../../../utils/dom-util';
+import {isShiftPressed, modifierPressed} from '../../../utils/dom-util';
import {ScrollMode} from '../../../constants/constants';
const NUMBER_FIXED_COLUMNS = 3;
@@ -57,6 +56,19 @@
const LABEL_PREFIX_INVALID_PROLOG = 'Invalid-Prolog-Rules-Label-Name--';
const MAX_SHORTCUT_CHARS = 5;
+export const columnNames = [
+ 'Subject',
+ 'Status',
+ 'Owner',
+ 'Assignee',
+ 'Reviewers',
+ 'Comments',
+ 'Repo',
+ 'Branch',
+ 'Updated',
+ 'Size',
+];
+
export interface ChangeListSection {
name?: string;
query?: string;
@@ -68,7 +80,7 @@
}
// This avoids JSC_DYNAMIC_EXTENDS_WITHOUT_JSDOC closure compiler error.
-const base = ChangeTableMixin(KeyboardShortcutMixin(PolymerElement));
+const base = KeyboardShortcutMixin(PolymerElement);
@customElement('gr-change-list')
export class GrChangeList extends base {
@@ -145,6 +157,8 @@
private readonly restApiService = appContext.restApiService;
+ private readonly shortcuts = appContext.shortcutsService;
+
override keyboardShortcuts() {
return {
[Shortcut.CURSOR_NEXT_CHANGE]: '_nextChange',
@@ -164,9 +178,7 @@
super();
this.cursor.scrollMode = ScrollMode.KEEP_VISIBLE;
this.cursor.focusOnMove = true;
- this.addEventListener('keydown', e =>
- this._scopedKeydownHandler(e as unknown as CustomKeyboardEvent)
- );
+ this.addEventListener('keydown', e => this._scopedKeydownHandler(e));
}
override ready() {
@@ -198,10 +210,10 @@
*
* Context: Issue 7294
*/
- _scopedKeydownHandler(e: CustomKeyboardEvent) {
+ _scopedKeydownHandler(e: KeyboardEvent) {
if (e.keyCode === 13) {
// Enter.
- this._openChange(e);
+ this.openChange(e);
}
}
@@ -219,32 +231,44 @@
return;
}
- this.changeTableColumns = this.columnNames;
+ this.changeTableColumns = columnNames;
this.showNumber = false;
- this.visibleChangeTableColumns = this.getEnabledColumns(
- this.columnNames,
- config,
- this.flagsService.enabledExperiments
+ this.visibleChangeTableColumns = this.changeTableColumns.filter(col =>
+ this._isColumnEnabled(col, config, this.flagsService.enabledExperiments)
);
-
if (account && preferences) {
this.showNumber = !!(
preferences && preferences.legacycid_in_change_table
);
if (preferences.change_table && preferences.change_table.length > 0) {
- const prefColumns = this.renameProjectToRepoColumn(
- preferences.change_table
+ const prefColumns = preferences.change_table.map(column =>
+ column === 'Project' ? 'Repo' : column
);
- this.visibleChangeTableColumns = this.getEnabledColumns(
- prefColumns,
- config,
- this.flagsService.enabledExperiments
+ this.visibleChangeTableColumns = prefColumns.filter(col =>
+ this._isColumnEnabled(
+ col,
+ config,
+ this.flagsService.enabledExperiments
+ )
);
}
}
}
/**
+ * Is the column disabled by a server config or experiment? For example the
+ * assignee feature might be disabled and thus the corresponding column is
+ * also disabled.
+ *
+ */
+ _isColumnEnabled(column: string, config: ServerInfo, experiments: string[]) {
+ if (!config || !config.change) return true;
+ if (column === 'Assignee') return !!config.change.enable_assignee;
+ if (column === 'Comments') return experiments.includes('comments-column');
+ return true;
+ }
+
+ /**
* This methods allows us to customize the columns per section.
*
* @param visibleColumns are the columns according to configs and user prefs
@@ -382,8 +406,8 @@
);
}
- _nextChange(e: CustomKeyboardEvent) {
- if (this.shouldSuppressKeyboardShortcut(e) || this.modifierPressed(e)) {
+ _nextChange(e: IronKeyboardEvent) {
+ if (this.shortcuts.shouldSuppress(e) || this.modifierPressed(e)) {
return;
}
@@ -394,8 +418,8 @@
this.selectedIndex = this.cursor.index;
}
- _prevChange(e: CustomKeyboardEvent) {
- if (this.shouldSuppressKeyboardShortcut(e) || this.modifierPressed(e)) {
+ _prevChange(e: IronKeyboardEvent) {
+ if (this.shortcuts.shouldSuppress(e) || this.modifierPressed(e)) {
return;
}
@@ -406,19 +430,21 @@
this.selectedIndex = this.cursor.index;
}
- _openChange(e: CustomKeyboardEvent) {
- if (this.shouldSuppressKeyboardShortcut(e) || this.modifierPressed(e)) {
- return;
- }
+ _openChange(e: IronKeyboardEvent) {
+ if (this.modifierPressed(e)) return;
+ this.openChange(e.detail.keyboardEvent);
+ }
+ openChange(e: KeyboardEvent) {
+ if (this.shortcuts.shouldSuppress(e) || modifierPressed(e)) return;
e.preventDefault();
const change = this._changeForIndex(this.selectedIndex);
if (change) GerritNav.navigateToChange(change);
}
- _nextPage(e: CustomKeyboardEvent) {
+ _nextPage(e: IronKeyboardEvent) {
if (
- this.shouldSuppressKeyboardShortcut(e) ||
+ this.shortcuts.shouldSuppress(e) ||
(this.modifierPressed(e) && !isShiftPressed(e))
) {
return;
@@ -428,9 +454,9 @@
fireEvent(this, 'next-page');
}
- _prevPage(e: CustomKeyboardEvent) {
+ _prevPage(e: IronKeyboardEvent) {
if (
- this.shouldSuppressKeyboardShortcut(e) ||
+ this.shortcuts.shouldSuppress(e) ||
(this.modifierPressed(e) && !isShiftPressed(e))
) {
return;
@@ -445,8 +471,8 @@
);
}
- _toggleChangeReviewed(e: CustomKeyboardEvent) {
- if (this.shouldSuppressKeyboardShortcut(e) || this.modifierPressed(e)) {
+ _toggleChangeReviewed(e: IronKeyboardEvent) {
+ if (this.shortcuts.shouldSuppress(e) || this.modifierPressed(e)) {
return;
}
@@ -464,8 +490,8 @@
changeEl.toggleReviewed();
}
- _refreshChangeList(e: CustomKeyboardEvent) {
- if (this.shouldSuppressKeyboardShortcut(e)) {
+ _refreshChangeList(e: IronKeyboardEvent) {
+ if (this.shortcuts.shouldSuppress(e)) {
return;
}
@@ -473,8 +499,8 @@
fireReload(this);
}
- _toggleChangeStar(e: CustomKeyboardEvent) {
- if (this.shouldSuppressKeyboardShortcut(e) || this.modifierPressed(e)) {
+ _toggleChangeStar(e: IronKeyboardEvent) {
+ if (this.shortcuts.shouldSuppress(e) || this.modifierPressed(e)) {
return;
}
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list_test.js b/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list_test.js
index 0d2056a..4956380 100644
--- a/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list_test.js
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list_test.js
@@ -19,8 +19,7 @@
import './gr-change-list.js';
import {afterNextRender} from '@polymer/polymer/lib/utils/render-status.js';
import {GerritNav} from '../../core/gr-navigation/gr-navigation.js';
-import {mockPromise, TestKeyboardShortcutBinder} from '../../../test/test-utils.js';
-import {Shortcut} from '../../../mixins/keyboard-shortcut-mixin/keyboard-shortcut-mixin.js';
+import {mockPromise} from '../../../test/test-utils.js';
import {YOUR_TURN} from '../../core/gr-navigation/gr-navigation.js';
const basicFixture = fixtureFromElement('gr-change-list');
@@ -28,22 +27,6 @@
suite('gr-change-list basic tests', () => {
let element;
- suiteSetup(() => {
- const kb = TestKeyboardShortcutBinder.push();
- kb.bindShortcut(Shortcut.CURSOR_NEXT_CHANGE, 'j');
- kb.bindShortcut(Shortcut.CURSOR_PREV_CHANGE, 'k');
- kb.bindShortcut(Shortcut.OPEN_CHANGE, 'o');
- kb.bindShortcut(Shortcut.REFRESH_CHANGE_LIST, 'shift+r');
- kb.bindShortcut(Shortcut.TOGGLE_CHANGE_REVIEWED, 'r');
- kb.bindShortcut(Shortcut.TOGGLE_CHANGE_STAR, 's');
- kb.bindShortcut(Shortcut.NEXT_PAGE, 'n');
- kb.bindShortcut(Shortcut.NEXT_PAGE, 'p');
- });
-
- suiteTeardown(() => {
- TestKeyboardShortcutBinder.pop();
- });
-
setup(() => {
element = basicFixture.instantiate();
});
@@ -269,7 +252,7 @@
});
test('all columns visible', () => {
- for (const column of element.columnNames) {
+ for (const column of element.changeTableColumns) {
const elementClass = '.' + element._lowerCase(column);
assert.isFalse(element.shadowRoot
.querySelector(elementClass).hidden);
@@ -495,11 +478,11 @@
assert.deepEqual(navStub.lastCall.args[0], {_number: 4},
'Should navigate to /c/4/');
- MockInteractions.pressAndReleaseKeyOn(element, 82); // 'r'
+ MockInteractions.keyUpOn(element, 82); // 'r'
const change = element._changeForIndex(element.selectedIndex);
assert.equal(change.reviewed, true,
'Should mark change as reviewed');
- MockInteractions.pressAndReleaseKeyOn(element, 82); // 'r'
+ MockInteractions.keyUpOn(element, 82); // 'r'
assert.equal(change.reviewed, false,
'Should mark change as unreviewed');
promise.resolve();
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 456b7cf..9eca3bc 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
@@ -14,6 +14,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
+import '../../../styles/gr-a11y-styles';
import '../../../styles/shared-styles';
import '../gr-change-list/gr-change-list';
import '../../shared/gr-button/gr-button';
diff --git a/polygerrit-ui/app/elements/change-list/gr-dashboard-view/gr-dashboard-view_html.ts b/polygerrit-ui/app/elements/change-list/gr-dashboard-view/gr-dashboard-view_html.ts
index 484a952..a55befb 100644
--- a/polygerrit-ui/app/elements/change-list/gr-dashboard-view/gr-dashboard-view_html.ts
+++ b/polygerrit-ui/app/elements/change-list/gr-dashboard-view/gr-dashboard-view_html.ts
@@ -17,6 +17,9 @@
import {html} from '@polymer/polymer/lib/utils/html-tag';
export const htmlTemplate = html`
+ <style include="gr-a11y-styles">
+ /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
+ </style>
<style include="shared-styles">
:host {
display: block;
@@ -39,11 +42,6 @@
justify-content: space-between;
padding: var(--spacing-xs) var(--spacing-l);
}
- .banner gr-button {
- --gr-button: {
- color: var(--primary-text-color);
- }
- }
.hide {
display: none;
}
diff --git a/polygerrit-ui/app/elements/change-list/gr-dashboard-view/gr-dashboard-view_test.js b/polygerrit-ui/app/elements/change-list/gr-dashboard-view/gr-dashboard-view_test.js
index 96a19e0..aa76347 100644
--- a/polygerrit-ui/app/elements/change-list/gr-dashboard-view/gr-dashboard-view_test.js
+++ b/polygerrit-ui/app/elements/change-list/gr-dashboard-view/gr-dashboard-view_test.js
@@ -128,7 +128,7 @@
// Open confirmation dialog and tap confirm button.
await element.$.confirmDeleteOverlay.open();
- MockInteractions.tap(element.$.confirmDeleteDialog.$.confirm);
+ MockInteractions.tap(element.$.confirmDeleteDialog.confirmButton);
flush();
assert.isTrue(deleteStub.calledWithExactly('-is:open'));
assert.isTrue(element.$.confirmDeleteDialog.disabled);
diff --git a/polygerrit-ui/app/elements/change-list/gr-repo-header/gr-repo-header.ts b/polygerrit-ui/app/elements/change-list/gr-repo-header/gr-repo-header.ts
index beadea3..d8949ed 100644
--- a/polygerrit-ui/app/elements/change-list/gr-repo-header/gr-repo-header.ts
+++ b/polygerrit-ui/app/elements/change-list/gr-repo-header/gr-repo-header.ts
@@ -15,7 +15,6 @@
* limitations under the License.
*/
-import '../../shared/gr-date-formatter/gr-date-formatter';
import {GerritNav} from '../../core/gr-navigation/gr-navigation';
import {RepoName} from '../../../types/common';
import {WebLinkInfo} from '../../../types/diff';
@@ -73,7 +72,7 @@
<h1 class="heading-1">${this.repo}</h1>
<hr />
<div>
- <span>Detail:</span> <a href="${this._repoUrl}">Repo settings</a>
+ <span>Detail:</span> <a href="${this._repoUrl!}">Repo settings</a>
</div>
${this._renderLinks(this._webLinks)}
</div>`;
diff --git a/polygerrit-ui/app/elements/change-list/gr-user-header/gr-user-header.ts b/polygerrit-ui/app/elements/change-list/gr-user-header/gr-user-header.ts
index bc21dc0..50de7b9 100644
--- a/polygerrit-ui/app/elements/change-list/gr-user-header/gr-user-header.ts
+++ b/polygerrit-ui/app/elements/change-list/gr-user-header/gr-user-header.ts
@@ -48,7 +48,7 @@
private readonly restApiService = appContext.restApiService;
- static get styles() {
+ static override get styles() {
return [
sharedStyles,
dashboardHeaderStyles,
@@ -63,8 +63,8 @@
];
}
- render() {
- return html` <gr-avatar
+ override render() {
+ return html`<gr-avatar
.account="${this._accountDetails}"
.imageSize=${100}
aria-label="Account avatar"
@@ -85,7 +85,7 @@
<div>
<span>Joined:</span>
<gr-date-formatter
- date-str="${this._computeDetail(
+ dateStr="${this._computeDetail(
this._accountDetails,
'registered_on'
)}"
@@ -95,10 +95,10 @@
<gr-endpoint-decorator name="user-header">
<gr-endpoint-param
name="accountDetails"
- value="${this._accountDetails}"
+ .value="${this._accountDetails}"
>
</gr-endpoint-param>
- <gr-endpoint-param name="loggedIn" value="${this.loggedIn}">
+ <gr-endpoint-param name="loggedIn" .value="${this.loggedIn}">
</gr-endpoint-param>
</gr-endpoint-decorator>
</div>
@@ -116,7 +116,7 @@
</div>`;
}
- updated(changedProperties: PropertyValues) {
+ override updated(changedProperties: PropertyValues) {
if (changedProperties.has('userId')) {
this._accountChanged(this.userId);
}
diff --git a/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions_html.ts b/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions_html.ts
index 614339a..d21c29f 100644
--- a/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions_html.ts
+++ b/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions_html.ts
@@ -33,6 +33,9 @@
/* px because don't have the same font size */
margin-left: 8px;
}
+ gr-button {
+ display: block;
+ }
#actionLoadingMessage {
align-items: center;
color: var(--deemphasized-text-color);
@@ -57,10 +60,8 @@
flex-wrap: wrap;
}
gr-button {
- --gr-button: {
- padding: var(--spacing-m);
- white-space: nowrap;
- }
+ --gr-button-padding: var(--spacing-m);
+ white-space: nowrap;
}
gr-button,
gr-dropdown {
@@ -84,24 +85,27 @@
hidden$="[[_shouldHideActions(_topLevelActions.*, _loading)]]"
>
<template is="dom-repeat" items="[[_topLevelPrimaryActions]]" as="action">
- <gr-button
- link=""
+ <gr-tooltip-content
title$="[[action.title]]"
has-tooltip="[[_computeHasTooltip(action.title)]]"
position-below="true"
- data-action-key$="[[action.__key]]"
- class$="[[action.__key]]"
- data-action-type$="[[action.__type]]"
- data-label$="[[action.label]]"
- disabled$="[[_calculateDisabled(action, _hasKnownChainState)]]"
- on-click="_handleActionTap"
>
- <iron-icon
- class$="[[_computeHasIcon(action)]]"
- icon$="gr-icons:[[action.icon]]"
- ></iron-icon>
- [[action.label]]
- </gr-button>
+ <gr-button
+ link=""
+ data-action-key$="[[action.__key]]"
+ class$="[[action.__key]]"
+ data-action-type$="[[action.__type]]"
+ data-label$="[[action.label]]"
+ disabled$="[[_calculateDisabled(action, _hasKnownChainState)]]"
+ on-click="_handleActionTap"
+ >
+ <iron-icon
+ class$="[[_computeHasIcon(action)]]"
+ icon$="gr-icons:[[action.icon]]"
+ ></iron-icon>
+ [[action.label]]
+ </gr-button>
+ </gr-tooltip-content>
</template>
</section>
<section
@@ -113,24 +117,27 @@
items="[[_topLevelSecondaryActions]]"
as="action"
>
- <gr-button
- link=""
+ <gr-tooltip-content
title$="[[action.title]]"
has-tooltip="[[_computeHasTooltip(action.title)]]"
position-below="true"
- data-action-key$="[[action.__key]]"
- class$="[[action.__key]]"
- data-action-type$="[[action.__type]]"
- data-label$="[[action.label]]"
- disabled$="[[_calculateDisabled(action, _hasKnownChainState)]]"
- on-click="_handleActionTap"
>
- <iron-icon
- class$="[[_computeHasIcon(action)]]"
- icon$="gr-icons:[[action.icon]]"
- ></iron-icon>
- [[action.label]]
- </gr-button>
+ <gr-button
+ link=""
+ data-action-key$="[[action.__key]]"
+ class$="[[action.__key]]"
+ data-action-type$="[[action.__type]]"
+ data-label$="[[action.label]]"
+ disabled$="[[_calculateDisabled(action, _hasKnownChainState)]]"
+ on-click="_handleActionTap"
+ >
+ <iron-icon
+ class$="[[_computeHasIcon(action)]]"
+ icon$="gr-icons:[[action.icon]]"
+ ></iron-icon>
+ [[action.label]]
+ </gr-button>
+ </gr-tooltip-content>
</template>
</section>
<gr-button hidden$="[[!_loading]]" disabled=""
diff --git a/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions_test.ts b/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions_test.ts
index 04326a6..26e2fb4 100644
--- a/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions_test.ts
+++ b/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions_test.ts
@@ -1094,7 +1094,7 @@
);
tap(abandonButton);
- assert.isUndefined(element.$.confirmAbandonDialog.message);
+ assert.equal(element.$.confirmAbandonDialog.message, '');
});
test('works', () => {
diff --git a/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.ts b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.ts
index 08ad2bd..8b46cd8 100644
--- a/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.ts
+++ b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.ts
@@ -15,6 +15,7 @@
* limitations under the License.
*/
import '../../../styles/shared-styles';
+import '../../../styles/gr-font-styles';
import '../../../styles/gr-change-metadata-shared-styles';
import '../../../styles/gr-change-view-integration-shared-styles';
import '../../plugins/gr-endpoint-decorator/gr-endpoint-decorator';
@@ -751,6 +752,16 @@
})
);
}
+
+ _showNewSubmitRequirements(change?: ParsedChangeInfo) {
+ if (!this._isSubmitRequirementsUiEnabled) return false;
+ return (change?.submit_requirements ?? []).length > 0;
+ }
+
+ _showNewSubmitRequirementWarning(change?: ParsedChangeInfo) {
+ if (!this._isSubmitRequirementsUiEnabled) return false;
+ return (change?.submit_requirements ?? []).length === 0;
+ }
}
declare global {
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 c080345..97101f6 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
@@ -20,6 +20,9 @@
<style include="gr-change-metadata-shared-styles">
/* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
</style>
+ <style include="gr-font-styles">
+ /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
+ </style>
<style include="shared-styles">
:host {
display: table;
@@ -94,7 +97,6 @@
max-width: 285px;
}
.metadata-title {
- font-weight: var(--font-weight-bold);
color: var(--deemphasized-text-color);
padding-left: var(--metadata-horizontal-padding);
}
@@ -111,10 +113,14 @@
--iron-icon-height: 18px;
--iron-icon-width: 18px;
}
+ .submit-requirement-error {
+ color: var(--deemphasized-text-color);
+ padding-left: var(--metadata-horizontal-padding);
+ }
</style>
<gr-external-style id="externalStyle" name="change-metadata">
<div class="metadata-header">
- <h3 class="metadata-title">Change Info</h3>
+ <h3 class="metadata-title heading-3">Change Info</h3>
<gr-button link="" class="show-all-button" on-click="_onShowAllClick"
>[[_computeShowAllLabelText(_showAllSections)]]
<iron-icon
@@ -134,9 +140,9 @@
<span class="title">Submitted</span>
<span class="value">
<gr-date-formatter
- has-tooltip=""
+ withTooltip
date-str="[[change.submitted]]"
- show-yesterday=""
+ showYesterday=""
></gr-date-formatter>
</span>
</section>
@@ -154,9 +160,9 @@
</span>
<span class="value">
<gr-date-formatter
- has-tooltip=""
+ withTooltip
date-str="[[change.updated]]"
- show-yesterday=""
+ showYesterday
></gr-date-formatter>
</span>
</section>
@@ -354,8 +360,8 @@
></gr-commit-info>
<gr-tooltip-content
id="parentNotCurrentMessage"
- has-tooltip=""
- show-icon=""
+ has-tooltip
+ show-icon
title$="[[_notCurrentMessage]]"
></gr-tooltip-content>
</li>
@@ -478,20 +484,25 @@
</span>
</section>
<div class="separatedSection">
- <template is="dom-if" if="[[_isSubmitRequirementsUiEnabled]]">
+ <template is="dom-if" if="[[_showNewSubmitRequirements(change)]]">
<gr-submit-requirements
change="[[change]]"
account="[[account]]"
mutable="[[_mutable]]"
></gr-submit-requirements>
</template>
- <template is="dom-if" if="[[!_isSubmitRequirementsUiEnabled]]">
+ <template is="dom-if" if="[[!_showNewSubmitRequirements(change)]]">
<gr-change-requirements
change="{{change}}"
account="[[account]]"
mutable="[[_mutable]]"
></gr-change-requirements>
</template>
+ <template is="dom-if" if="[[_showNewSubmitRequirementWarning(change)]]">
+ <div class="submit-requirement-error">
+ New Submit Requirements don't work on this change.
+ </div>
+ </template>
</div>
<section
id="webLinks"
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 6a31c0a..725bb24 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
@@ -15,9 +15,9 @@
* limitations under the License.
*/
import '../../../styles/shared-styles';
+import '../../../styles/gr-font-styles';
import '../../shared/gr-button/gr-button';
import '../../shared/gr-icons/gr-icons';
-import '../../shared/gr-label/gr-label';
import '../../shared/gr-label-info/gr-label-info';
import '../../shared/gr-limited-text/gr-limited-text';
import {PolymerElement} from '@polymer/polymer/polymer-element';
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 d824d94..8161592 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
@@ -17,6 +17,9 @@
import {html} from '@polymer/polymer/lib/utils/html-tag';
export const htmlTemplate = html`
+ <style include="gr-font-styles">
+ /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
+ </style>
<style include="shared-styles">
:host {
display: table;
@@ -104,7 +107,7 @@
padding-left: 0;
}
</style>
- <h3 class="metadata-title">Submit requirements</h3>
+ <h3 class="metadata-title heading-3">Submit requirements</h3>
<template is="dom-repeat" items="[[_requirements]]">
<gr-endpoint-decorator
class="submit-requirement-endpoints"
diff --git a/polygerrit-ui/app/elements/change/gr-change-summary/gr-change-summary.ts b/polygerrit-ui/app/elements/change/gr-change-summary/gr-change-summary.ts
index 831a309..ad8f72f 100644
--- a/polygerrit-ui/app/elements/change/gr-change-summary/gr-change-summary.ts
+++ b/polygerrit-ui/app/elements/change/gr-change-summary/gr-change-summary.ts
@@ -33,6 +33,7 @@
import {Action, Category, Link, RunStatus} from '../../../api/checks';
import {fireShowPrimaryTab} from '../../../utils/event-util';
import '../../shared/gr-avatar/gr-avatar';
+import '../../checks/gr-checks-action';
import {
firstPrimaryLink,
getResultsOf,
@@ -289,8 +290,6 @@
color: var(--success-foreground);
}
.checksChip.timelapse {
- }
- .checksChip.timelapse {
border-color: var(--gray-foreground);
background: var(--gray-background);
}
@@ -514,7 +513,7 @@
.actions gr-checks-action,
.actions gr-dropdown {
vertical-align: top;
- --padding: 0 var(--spacing-m);
+ --gr-button-padding: 0 var(--spacing-m);
}
.actions #moreMessage {
display: none;
@@ -662,7 +661,7 @@
const handler = () => this.onChipClick({statusOrCategory});
return html`<gr-checks-chip
.statusOrCategory="${statusOrCategory}"
- .text="${count}"
+ .text="${`${count}`}"
@click="${handler}"
@keydown="${(e: KeyboardEvent) => handleSpaceOrEnter(e, handler)}"
></gr-checks-chip>`;
diff --git a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.ts b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.ts
index 58a00f4..fc2cbe5 100644
--- a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.ts
+++ b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.ts
@@ -15,6 +15,7 @@
* limitations under the License.
*/
import '@polymer/paper-tabs/paper-tabs';
+import '../../../styles/gr-a11y-styles';
import '../../../styles/shared-styles';
import '../../diff/gr-comment-api/gr-comment-api';
import '../../plugins/gr-endpoint-decorator/gr-endpoint-decorator';
@@ -23,7 +24,6 @@
import '../../shared/gr-button/gr-button';
import '../../shared/gr-change-star/gr-change-star';
import '../../shared/gr-change-status/gr-change-status';
-import '../../shared/gr-date-formatter/gr-date-formatter';
import '../../shared/gr-editable-content/gr-editable-content';
import '../../shared/gr-linked-text/gr-linked-text';
import '../../shared/gr-overlay/gr-overlay';
@@ -48,10 +48,11 @@
import {
KeyboardShortcutMixin,
Shortcut,
+ ShortcutSection,
} from '../../../mixins/keyboard-shortcut-mixin/keyboard-shortcut-mixin';
import {GrEditConstants} from '../../edit/gr-edit-constants';
import {pluralize} from '../../../utils/string-util';
-import {windowLocationReload} from '../../../utils/dom-util';
+import {windowLocationReload, querySelectorAll} from '../../../utils/dom-util';
import {
GeneratedWebLink,
GerritNav,
@@ -84,6 +85,7 @@
isCc,
isOwner,
isReviewer,
+ isInvolved,
} from '../../../utils/change-util';
import {EventType as PluginEventType} from '../../../api/plugin';
import {customElement, observe, property} from '@polymer/decorators';
@@ -156,8 +158,9 @@
ParsedChangeInfo,
} from '../../../types/types';
import {
+ IronKeyboardEventListener,
CloseFixPreviewEvent,
- CustomKeyboardEvent,
+ IronKeyboardEvent,
EditableContentSaveEvent,
EventType,
OpenFixPreviewEvent,
@@ -188,6 +191,11 @@
changeComments$,
drafts$,
} from '../../../services/comments/comments-model';
+import {
+ hasAttention,
+ getAddedByReason,
+ getRemovedByReason,
+} from '../../../utils/attention-set-util';
const MIN_LINES_FOR_COMMIT_COLLAPSE = 18;
@@ -530,7 +538,7 @@
@property({type: Boolean})
_showRobotCommentsButton = false;
- _throttledToggleChangeStar?: EventListener;
+ _throttledToggleChangeStar?: IronKeyboardEventListener;
@property({type: Boolean})
_showChecksTab = false;
@@ -557,6 +565,8 @@
private readonly commentsService = appContext.commentsService;
+ private readonly shortcuts = appContext.shortcutsService;
+
private replyDialogResizeObserver?: ResizeObserver;
override keyboardShortcuts() {
@@ -579,6 +589,7 @@
[Shortcut.DIFF_RIGHT_AGAINST_LATEST]: '_handleDiffRightAgainstLatest',
[Shortcut.DIFF_BASE_AGAINST_LATEST]: '_handleDiffBaseAgainstLatest',
[Shortcut.OPEN_SUBMIT_DIALOG]: '_handleOpenSubmitDialog',
+ [Shortcut.TOGGLE_ATTENTION_SET]: '_handleToggleAttentionSet',
};
}
@@ -630,8 +641,8 @@
override connectedCallback() {
super.connectedCallback();
- this._throttledToggleChangeStar = throttleWrap(e =>
- this._handleToggleChangeStar(e as CustomKeyboardEvent)
+ this._throttledToggleChangeStar = throttleWrap<IronKeyboardEvent>(e =>
+ this._handleToggleChangeStar(e)
);
this._getServerConfig().then(config => {
this._serverConfig = config;
@@ -740,8 +751,8 @@
if (e.detail.fixApplied) fireReload(this);
}
- _handleToggleDiffMode(e: CustomKeyboardEvent) {
- if (this.shouldSuppressKeyboardShortcut(e) || this.modifierPressed(e)) {
+ _handleToggleDiffMode(e: IronKeyboardEvent) {
+ if (this.shortcuts.shouldSuppress(e) || this.modifierPressed(e)) {
return;
}
@@ -1172,6 +1183,9 @@
_paramsChanged(value: AppElementChangeViewParams) {
if (value.view !== GerritView.CHANGE) {
this._initialLoadComplete = false;
+ querySelectorAll(this, 'gr-overlay').forEach(overlay =>
+ (overlay as GrOverlay).close()
+ );
return;
}
@@ -1481,8 +1495,8 @@
return label;
}
- _handleOpenReplyDialog(e: CustomKeyboardEvent) {
- if (this.shouldSuppressKeyboardShortcut(e) || this.modifierPressed(e)) {
+ _handleOpenReplyDialog(e: IronKeyboardEvent) {
+ if (this.shortcuts.shouldSuppress(e) || this.modifierPressed(e)) {
return;
}
this._getLoggedIn().then(isLoggedIn => {
@@ -1496,8 +1510,8 @@
});
}
- _handleOpenDownloadDialogShortcut(e: CustomKeyboardEvent) {
- if (this.shouldSuppressKeyboardShortcut(e) || this.modifierPressed(e)) {
+ _handleOpenDownloadDialogShortcut(e: IronKeyboardEvent) {
+ if (this.shortcuts.shouldSuppress(e) || this.modifierPressed(e)) {
return;
}
@@ -1505,8 +1519,8 @@
this._handleOpenDownloadDialog();
}
- _handleEditTopic(e: CustomKeyboardEvent) {
- if (this.shouldSuppressKeyboardShortcut(e) || this.modifierPressed(e)) {
+ _handleEditTopic(e: IronKeyboardEvent) {
+ if (this.shortcuts.shouldSuppress(e) || this.modifierPressed(e)) {
return;
}
@@ -1514,8 +1528,8 @@
this.$.metadata.editTopic();
}
- _handleOpenSubmitDialog(e: CustomKeyboardEvent) {
- if (this.shouldSuppressKeyboardShortcut(e) || !this._submitEnabled) {
+ _handleOpenSubmitDialog(e: IronKeyboardEvent) {
+ if (this.shortcuts.shouldSuppress(e) || !this._submitEnabled) {
return;
}
@@ -1523,8 +1537,50 @@
this.$.actions.showSubmitDialog();
}
- _handleDiffAgainstBase(e: CustomKeyboardEvent) {
- if (this.shouldSuppressKeyboardShortcut(e)) {
+ _handleToggleAttentionSet(e: IronKeyboardEvent) {
+ if (this.shortcuts.shouldSuppress(e)) {
+ return;
+ }
+ if (!this._change || !this._account?._account_id) return;
+ if (!this._loggedIn || !isInvolved(this._change, this._account)) return;
+ if (!this._change.attention_set) this._change.attention_set = {};
+ if (hasAttention(this._account, this._change)) {
+ const reason = getRemovedByReason(this._account, this._serverConfig);
+ if (this._change.attention_set)
+ delete this._change.attention_set[this._account._account_id];
+ fireAlert(this, 'Removing you from the attention set ...');
+ this.restApiService
+ .removeFromAttentionSet(
+ this._change._number,
+ this._account._account_id,
+ reason
+ )
+ .then(() => {
+ fireEvent(this, 'hide-alert');
+ });
+ } else {
+ const reason = getAddedByReason(this._account, this._serverConfig);
+ fireAlert(this, 'Adding you to the attention set ...');
+ this._change.attention_set[this._account._account_id!] = {
+ account: this._account,
+ reason,
+ reason_account: this._account,
+ };
+ this.restApiService
+ .addToAttentionSet(
+ this._change._number,
+ this._account._account_id,
+ reason
+ )
+ .then(() => {
+ fireEvent(this, 'hide-alert');
+ });
+ }
+ this._change = {...this._change};
+ }
+
+ _handleDiffAgainstBase(e: IronKeyboardEvent) {
+ if (this.shortcuts.shouldSuppress(e)) {
return;
}
assertIsDefined(this._change, '_change');
@@ -1537,8 +1593,8 @@
GerritNav.navigateToChange(this._change, this._patchRange.patchNum);
}
- _handleDiffBaseAgainstLeft(e: CustomKeyboardEvent) {
- if (this.shouldSuppressKeyboardShortcut(e)) {
+ _handleDiffBaseAgainstLeft(e: IronKeyboardEvent) {
+ if (this.shortcuts.shouldSuppress(e)) {
return;
}
assertIsDefined(this._change, '_change');
@@ -1551,8 +1607,8 @@
GerritNav.navigateToChange(this._change, this._patchRange.basePatchNum);
}
- _handleDiffAgainstLatest(e: CustomKeyboardEvent) {
- if (this.shouldSuppressKeyboardShortcut(e)) {
+ _handleDiffAgainstLatest(e: IronKeyboardEvent) {
+ if (this.shortcuts.shouldSuppress(e)) {
return;
}
assertIsDefined(this._change, '_change');
@@ -1570,8 +1626,8 @@
);
}
- _handleDiffRightAgainstLatest(e: CustomKeyboardEvent) {
- if (this.shouldSuppressKeyboardShortcut(e)) {
+ _handleDiffRightAgainstLatest(e: IronKeyboardEvent) {
+ if (this.shortcuts.shouldSuppress(e)) {
return;
}
assertIsDefined(this._change, '_change');
@@ -1589,8 +1645,8 @@
);
}
- _handleDiffBaseAgainstLatest(e: CustomKeyboardEvent) {
- if (this.shouldSuppressKeyboardShortcut(e)) {
+ _handleDiffBaseAgainstLatest(e: IronKeyboardEvent) {
+ if (this.shortcuts.shouldSuppress(e)) {
return;
}
assertIsDefined(this._change, '_change');
@@ -1607,24 +1663,24 @@
GerritNav.navigateToChange(this._change, latestPatchNum);
}
- _handleRefreshChange(e: CustomKeyboardEvent) {
- if (this.shouldSuppressKeyboardShortcut(e)) {
+ _handleRefreshChange(e: IronKeyboardEvent) {
+ if (this.shortcuts.shouldSuppress(e)) {
return;
}
e.preventDefault();
fireReload(this, true);
}
- _handleToggleChangeStar(e: CustomKeyboardEvent) {
- if (this.shouldSuppressKeyboardShortcut(e) || this.modifierPressed(e)) {
+ _handleToggleChangeStar(e: IronKeyboardEvent) {
+ if (this.shortcuts.shouldSuppress(e) || this.modifierPressed(e)) {
return;
}
e.preventDefault();
this.$.changeStar.toggleStar();
}
- _handleUpToDashboard(e: CustomKeyboardEvent) {
- if (this.shouldSuppressKeyboardShortcut(e) || this.modifierPressed(e)) {
+ _handleUpToDashboard(e: IronKeyboardEvent) {
+ if (this.shortcuts.shouldSuppress(e) || this.modifierPressed(e)) {
return;
}
@@ -1632,8 +1688,8 @@
this._determinePageBack();
}
- _handleExpandAllMessages(e: CustomKeyboardEvent) {
- if (this.shouldSuppressKeyboardShortcut(e) || this.modifierPressed(e)) {
+ _handleExpandAllMessages(e: IronKeyboardEvent) {
+ if (this.shortcuts.shouldSuppress(e) || this.modifierPressed(e)) {
return;
}
@@ -1643,8 +1699,8 @@
}
}
- _handleCollapseAllMessages(e: CustomKeyboardEvent) {
- if (this.shouldSuppressKeyboardShortcut(e) || this.modifierPressed(e)) {
+ _handleCollapseAllMessages(e: IronKeyboardEvent) {
+ if (this.shortcuts.shouldSuppress(e) || this.modifierPressed(e)) {
return;
}
@@ -1654,8 +1710,8 @@
}
}
- _handleOpenDiffPrefsShortcut(e: CustomKeyboardEvent) {
- if (this.shouldSuppressKeyboardShortcut(e) || this.modifierPressed(e)) {
+ _handleOpenDiffPrefsShortcut(e: IronKeyboardEvent) {
+ if (this.shortcuts.shouldSuppress(e) || this.modifierPressed(e)) {
return;
}
@@ -1768,7 +1824,7 @@
changeIsOpen(change)
) {
fireAlert(this, 'Change edit not found. Please create a change edit.');
- GerritNav.navigateToChange(change);
+ fireReload(this, true);
return;
}
@@ -1781,7 +1837,7 @@
this,
'Change edits cannot be created if change is merged or abandoned. Redirected to non edit mode.'
);
- GerritNav.navigateToChange(change);
+ fireReload(this, true);
return;
}
@@ -2592,6 +2648,10 @@
'#relatedChanges'
);
}
+
+ createTitle(shortcutName: Shortcut, section: ShortcutSection) {
+ return this.shortcuts.createTitle(shortcutName, section);
+ }
}
declare global {
diff --git a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_html.ts b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_html.ts
index fb80822..d57aca8 100644
--- a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_html.ts
+++ b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_html.ts
@@ -17,6 +17,9 @@
import {html} from '@polymer/polymer/lib/utils/html-tag';
export const htmlTemplate = html`
+ <style include="gr-a11y-styles">
+ /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
+ </style>
<style include="shared-styles">
.container:not(.loading) {
background-color: var(--background-color-tertiary);
@@ -476,7 +479,7 @@
class="commentThreads"
>
<gr-tooltip-content
- has-tooltip=""
+ has-tooltip
title$="[[_computeTotalCommentCounts(_change.unresolved_comment_count, _changeComments)]]"
>
<span>Comments</span></gr-tooltip-content
diff --git a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_test.ts b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_test.ts
index 676e066..a82fceb 100644
--- a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_test.ts
+++ b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_test.ts
@@ -35,12 +35,7 @@
import {EventType, PluginApi} from '../../../api/plugin';
import 'lodash/lodash';
-import {
- mockPromise,
- stubRestApi,
- TestKeyboardShortcutBinder,
-} from '../../../test/test-utils';
-import {Shortcut} from '../../../mixins/keyboard-shortcut-mixin/keyboard-shortcut-mixin';
+import {mockPromise, stubRestApi} from '../../../test/test-utils';
import {
createAppElementChangeViewParams,
createApproval,
@@ -59,6 +54,7 @@
createAccountWithIdNameAndEmail,
createChangeViewChange,
createRelatedChangeAndCommitInfo,
+ createAccountDetailWithId,
} from '../../../test/test-data-generators';
import {ChangeViewPatchRange, GrChangeView} from './gr-change-view';
import {
@@ -86,13 +82,17 @@
} from '../../../types/common';
import {
pressAndReleaseKeyOn,
+ keyUpOn,
tap,
} from '@polymer/iron-test-helpers/mock-interactions';
import {GrEditControls} from '../../edit/gr-edit-controls/gr-edit-controls';
import {AppElementChangeViewParams} from '../../gr-app-types';
import {SinonFakeTimers, SinonStubbedMember} from 'sinon';
import {RestApiService} from '../../../services/gr-rest-api/gr-rest-api';
-import {CustomKeyboardEvent} from '../../../types/events';
+import {
+ IronKeyboardEvent,
+ IronKeyboardEventDetail,
+} from '../../../types/events';
import {CommentThread, UIRobot} from '../../../utils/comment-util';
import {GerritView} from '../../../services/router/router-model';
import {ParsedChangeInfo} from '../../../types/types';
@@ -110,25 +110,6 @@
typeof GerritNav.navigateToChange
>;
- suiteSetup(() => {
- const kb = TestKeyboardShortcutBinder.push();
- kb.bindShortcut(Shortcut.SEND_REPLY, 'ctrl+enter');
- kb.bindShortcut(Shortcut.REFRESH_CHANGE, 'shift+r');
- kb.bindShortcut(Shortcut.OPEN_REPLY_DIALOG, 'a');
- kb.bindShortcut(Shortcut.OPEN_DOWNLOAD_DIALOG, 'd');
- kb.bindShortcut(Shortcut.TOGGLE_DIFF_MODE, 'm');
- kb.bindShortcut(Shortcut.TOGGLE_CHANGE_STAR, 's');
- kb.bindShortcut(Shortcut.UP_TO_DASHBOARD, 'u');
- kb.bindShortcut(Shortcut.EXPAND_ALL_MESSAGES, 'x');
- kb.bindShortcut(Shortcut.COLLAPSE_ALL_MESSAGES, 'z');
- kb.bindShortcut(Shortcut.OPEN_DIFF_PREFS, ',');
- kb.bindShortcut(Shortcut.EDIT_TOPIC, 't');
- });
-
- suiteTeardown(() => {
- TestKeyboardShortcutBinder.pop();
- });
-
const ROBOT_COMMENTS_LIMIT = 10;
// TODO: should have a mock service to generate VALID fake data
@@ -366,7 +347,9 @@
},
})
);
- stubRestApi('getAccount').returns(Promise.resolve(undefined));
+ stubRestApi('getAccount').returns(
+ Promise.resolve(createAccountDetailWithId(5))
+ );
stubRestApi('getDiffComments').returns(Promise.resolve({}));
stubRestApi('getDiffRobotComments').returns(Promise.resolve({}));
stubRestApi('getDiffDrafts').returns(Promise.resolve({}));
@@ -420,8 +403,7 @@
patchNum: 3 as RevisionPatchSetNum,
basePatchNum: 1 as BasePatchSetNum,
};
- sinon.stub(element, 'shouldSuppressKeyboardShortcut').returns(false);
- element._handleDiffAgainstBase(new CustomEvent('') as CustomKeyboardEvent);
+ element._handleDiffAgainstBase(new CustomEvent('') as IronKeyboardEvent);
assert(navigateToChangeStub.called);
const args = navigateToChangeStub.getCall(0).args;
assert.equal(args[0], element._change);
@@ -437,10 +419,7 @@
basePatchNum: 1 as BasePatchSetNum,
patchNum: 3 as RevisionPatchSetNum,
};
- sinon.stub(element, 'shouldSuppressKeyboardShortcut').returns(false);
- element._handleDiffAgainstLatest(
- new CustomEvent('') as CustomKeyboardEvent
- );
+ element._handleDiffAgainstLatest(new CustomEvent('') as IronKeyboardEvent);
assert(navigateToChangeStub.called);
const args = navigateToChangeStub.getCall(0).args;
assert.equal(args[0], element._change);
@@ -457,9 +436,8 @@
patchNum: 3 as RevisionPatchSetNum,
basePatchNum: 1 as BasePatchSetNum,
};
- sinon.stub(element, 'shouldSuppressKeyboardShortcut').returns(false);
element._handleDiffBaseAgainstLeft(
- new CustomEvent('') as CustomKeyboardEvent
+ new CustomEvent('') as IronKeyboardEvent
);
assert(navigateToChangeStub.called);
const args = navigateToChangeStub.getCall(0).args;
@@ -476,9 +454,8 @@
basePatchNum: 1 as BasePatchSetNum,
patchNum: 3 as RevisionPatchSetNum,
};
- sinon.stub(element, 'shouldSuppressKeyboardShortcut').returns(false);
element._handleDiffRightAgainstLatest(
- new CustomEvent('') as CustomKeyboardEvent
+ new CustomEvent('') as IronKeyboardEvent
);
assert(navigateToChangeStub.called);
const args = navigateToChangeStub.getCall(0).args;
@@ -495,9 +472,8 @@
basePatchNum: 1 as BasePatchSetNum,
patchNum: 3 as RevisionPatchSetNum,
};
- sinon.stub(element, 'shouldSuppressKeyboardShortcut').returns(false);
element._handleDiffBaseAgainstLatest(
- new CustomEvent('') as CustomKeyboardEvent
+ new CustomEvent('') as IronKeyboardEvent
);
assert(navigateToChangeStub.called);
const args = navigateToChangeStub.getCall(0).args;
@@ -505,6 +481,34 @@
assert.isNotOk(args[2]);
});
+ test('toggle attention set status', async () => {
+ element._change = {
+ ...createChangeViewChange(),
+ revisions: createRevisions(10),
+ };
+ const addToAttentionSetStub = stubRestApi('addToAttentionSet').returns(
+ Promise.resolve(new Response())
+ );
+
+ const removeFromAttentionSetStub = stubRestApi(
+ 'removeFromAttentionSet'
+ ).returns(Promise.resolve(new Response()));
+ element._patchRange = {
+ basePatchNum: 1 as BasePatchSetNum,
+ patchNum: 3 as RevisionPatchSetNum,
+ };
+
+ assert.isNotOk(element._change.attention_set);
+ await element._getLoggedIn();
+ await element.restApiService.getAccount();
+ element._handleToggleAttentionSet(new CustomEvent('') as IronKeyboardEvent);
+ assert.isTrue(addToAttentionSetStub.called);
+ assert.isFalse(removeFromAttentionSetStub.called);
+
+ element._handleToggleAttentionSet(new CustomEvent('') as IronKeyboardEvent);
+ assert.isTrue(removeFromAttentionSetStub.called);
+ });
+
suite('plugins adding to file tab', () => {
setup(async () => {
element._changeNum = TEST_NUMERIC_CHANGE_ID;
@@ -646,7 +650,7 @@
sinon.stub(element, '_getLoggedIn').returns(Promise.resolve(false));
const loggedInErrorSpy = sinon.spy();
element.addEventListener('show-auth-required', loggedInErrorSpy);
- pressAndReleaseKeyOn(element, 65, null, 'a');
+ keyUpOn(element, 65, null, 'a');
await flush();
assert.isFalse(element.$.replyOverlay.opened);
assert.isTrue(loggedInErrorSpy.called);
@@ -679,7 +683,7 @@
const openSpy = sinon.spy(element, '_openReplyDialog');
- pressAndReleaseKeyOn(element, 65, null, 'a');
+ keyUpOn(element, 65, null, 'a');
await flush();
assert.isTrue(element.$.replyOverlay.opened);
element.$.replyOverlay.close();
@@ -792,7 +796,7 @@
const stub = sinon
.stub(element.$.downloadOverlay, 'open')
.returns(Promise.resolve());
- pressAndReleaseKeyOn(element, 68, null, 'd');
+ keyUpOn(element, 68, null, 'd');
assert.isTrue(stub.called);
});
@@ -816,12 +820,13 @@
});
test('m should toggle diff mode', () => {
- sinon.stub(element, 'shouldSuppressKeyboardShortcut').returns(false);
const setModeStub = sinon.stub(
element.$.fileListHeader,
'setDiffViewMode'
);
- const e = {preventDefault: () => {}} as CustomKeyboardEvent;
+ const e = new CustomEvent<IronKeyboardEventDetail>('keydown', {
+ detail: {keyboardEvent: new KeyboardEvent('keydown'), key: 'x'},
+ });
flush();
element.viewState.diffMode = DiffViewMode.SIDE_BY_SIDE;
diff --git a/polygerrit-ui/app/elements/change/gr-commit-info/gr-commit-info.ts b/polygerrit-ui/app/elements/change/gr-commit-info/gr-commit-info.ts
index 1052c91..0ce3b07 100644
--- a/polygerrit-ui/app/elements/change/gr-commit-info/gr-commit-info.ts
+++ b/polygerrit-ui/app/elements/change/gr-commit-info/gr-commit-info.ts
@@ -43,7 +43,7 @@
@property({type: Object})
serverConfig: ServerInfo | undefined;
- static get styles() {
+ static override get styles() {
return [
sharedStyles,
css`
@@ -55,7 +55,7 @@
];
}
- render() {
+ override render() {
return html` <div class="container">
<a
target="_blank"
@@ -73,9 +73,9 @@
)}</a
>
<gr-copy-clipboard
- hasTooltip=""
- .buttonTitle="Copy full SHA to clipboard"
- hideInput=""
+ hastooltip
+ .buttonTitle="${'Copy full SHA to clipboard'}"
+ hideinput
.text="${this.commitInfo?.commit}"
>
</gr-copy-clipboard>
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-abandon-dialog/gr-confirm-abandon-dialog.ts b/polygerrit-ui/app/elements/change/gr-confirm-abandon-dialog/gr-confirm-abandon-dialog.ts
index 4fd42b0..07da53f 100644
--- a/polygerrit-ui/app/elements/change/gr-confirm-abandon-dialog/gr-confirm-abandon-dialog.ts
+++ b/polygerrit-ui/app/elements/change/gr-confirm-abandon-dialog/gr-confirm-abandon-dialog.ts
@@ -57,7 +57,7 @@
*/
@property({type: String})
- message?: string;
+ message = '';
get keyBindings() {
return {
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-abandon-dialog/gr-confirm-abandon-dialog_test.js b/polygerrit-ui/app/elements/change/gr-confirm-abandon-dialog/gr-confirm-abandon-dialog_test.js
deleted file mode 100644
index 14d16f5..0000000
--- a/polygerrit-ui/app/elements/change/gr-confirm-abandon-dialog/gr-confirm-abandon-dialog_test.js
+++ /dev/null
@@ -1,62 +0,0 @@
-/**
- * @license
- * Copyright (C) 2017 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 './gr-confirm-abandon-dialog.js';
-
-const basicFixture = fixtureFromElement('gr-confirm-abandon-dialog');
-
-suite('gr-confirm-abandon-dialog tests', () => {
- let element;
-
- setup(() => {
- element = basicFixture.instantiate();
- });
-
- test('_handleConfirmTap', () => {
- const confirmHandler = sinon.stub();
- element.addEventListener('confirm', confirmHandler);
- sinon.spy(element, '_handleConfirmTap');
- sinon.spy(element, '_confirm');
- element.shadowRoot
- .querySelector('gr-dialog').dispatchEvent(
- new CustomEvent('confirm', {
- composed: true, bubbles: true,
- }));
- assert.isTrue(confirmHandler.called);
- assert.isTrue(confirmHandler.calledOnce);
- assert.isTrue(element._handleConfirmTap.called);
- assert.isTrue(element._confirm.called);
- assert.isTrue(element._confirm.calledOnce);
- });
-
- test('_handleCancelTap', () => {
- const cancelHandler = sinon.stub();
- element.addEventListener('cancel', cancelHandler);
- sinon.spy(element, '_handleCancelTap');
- element.shadowRoot
- .querySelector('gr-dialog').dispatchEvent(
- new CustomEvent('cancel', {
- composed: true, bubbles: true,
- }));
- assert.isTrue(cancelHandler.called);
- assert.isTrue(cancelHandler.calledOnce);
- assert.isTrue(element._handleCancelTap.called);
- assert.isTrue(element._handleCancelTap.calledOnce);
- });
-});
-
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-abandon-dialog/gr-confirm-abandon-dialog_test.ts b/polygerrit-ui/app/elements/change/gr-confirm-abandon-dialog/gr-confirm-abandon-dialog_test.ts
new file mode 100644
index 0000000..08dda14
--- /dev/null
+++ b/polygerrit-ui/app/elements/change/gr-confirm-abandon-dialog/gr-confirm-abandon-dialog_test.ts
@@ -0,0 +1,66 @@
+/**
+ * @license
+ * Copyright (C) 2017 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';
+import './gr-confirm-abandon-dialog';
+import {GrConfirmAbandonDialog} from './gr-confirm-abandon-dialog';
+import {queryAndAssert} from '../../../test/test-utils';
+import {GrDialog} from '../../shared/gr-dialog/gr-dialog';
+
+const basicFixture = fixtureFromElement('gr-confirm-abandon-dialog');
+
+suite('gr-confirm-abandon-dialog tests', () => {
+ let element: GrConfirmAbandonDialog;
+
+ setup(() => {
+ element = basicFixture.instantiate();
+ });
+
+ test('_handleConfirmTap', () => {
+ const confirmHandler = sinon.stub();
+ element.addEventListener('confirm', confirmHandler);
+ const confirmTapSpy = sinon.spy(element, '_handleConfirmTap');
+ const confirmSpy = sinon.spy(element, '_confirm');
+ queryAndAssert<GrDialog>(element, 'gr-dialog').dispatchEvent(
+ new CustomEvent('confirm', {
+ composed: true,
+ bubbles: true,
+ })
+ );
+ assert.isTrue(confirmHandler.called);
+ assert.isTrue(confirmHandler.calledOnce);
+ assert.isTrue(confirmTapSpy.called);
+ assert.isTrue(confirmSpy.called);
+ assert.isTrue(confirmSpy.calledOnce);
+ });
+
+ test('_handleCancelTap', () => {
+ const cancelHandler = sinon.stub();
+ element.addEventListener('cancel', cancelHandler);
+ const cancelTapSpy = sinon.spy(element, '_handleCancelTap');
+ queryAndAssert<GrDialog>(element, 'gr-dialog').dispatchEvent(
+ new CustomEvent('cancel', {
+ composed: true,
+ bubbles: true,
+ })
+ );
+ assert.isTrue(cancelHandler.called);
+ assert.isTrue(cancelHandler.calledOnce);
+ assert.isTrue(cancelTapSpy.called);
+ assert.isTrue(cancelTapSpy.calledOnce);
+ });
+});
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-dialog/gr-confirm-cherrypick-dialog.ts b/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-dialog/gr-confirm-cherrypick-dialog.ts
index fbf57f3..b061043 100644
--- a/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-dialog/gr-confirm-cherrypick-dialog.ts
+++ b/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-dialog/gr-confirm-cherrypick-dialog.ts
@@ -25,15 +25,14 @@
import {appContext} from '../../../services/app-context';
import {
ChangeInfo,
- BranchInfo,
- RepoName,
BranchName,
+ RepoName,
CommitId,
ChangeInfoId,
} from '../../../types/common';
import {ReportingService} from '../../../services/gr-reporting/gr-reporting';
import {customElement, property, observe} from '@polymer/decorators';
-import {AutocompleteSuggestion} from '../../shared/gr-autocomplete/gr-autocomplete';
+import {GrTypedAutocomplete} from '../../shared/gr-autocomplete/gr-autocomplete';
import {HttpMethod, ChangeStatus} from '../../../constants/constants';
import {dom, EventApi} from '@polymer/polymer/lib/legacy/polymer.dom';
import {fireEvent} from '../../../utils/event-util';
@@ -68,7 +67,7 @@
export interface GrConfirmCherrypickDialog {
$: {
- branchInput: HTMLElement;
+ branchInput: GrTypedAutocomplete<BranchName>;
};
}
@@ -91,7 +90,7 @@
*/
@property({type: String})
- branch?: BranchName;
+ branch = '' as BranchName;
@property({type: String})
baseCommit?: string;
@@ -106,7 +105,7 @@
commitNum?: CommitId;
@property({type: String})
- message?: string;
+ message = '';
@property({type: String})
project?: RepoName;
@@ -115,7 +114,7 @@
changes: ChangeInfo[] = [];
@property({type: Object})
- _query: (input: string) => Promise<AutocompleteSuggestion[]>;
+ _query?: (input: string) => Promise<{name: BranchName}[]>;
@property({type: Boolean})
_showCherryPickTopic = false;
@@ -249,7 +248,7 @@
cherryPickType: CherryPickType,
duplicateProjectChanges: boolean,
statuses: Statuses,
- branch?: BranchName
+ branch: BranchName
) {
if (!branch) return true;
const duplicateProject =
@@ -398,29 +397,22 @@
this.$.branchInput.focus();
}
- _getProjectBranchesSuggestions(
- input: string
- ): Promise<AutocompleteSuggestion[]> {
- if (!this.project) {
- this.reporting.error(new Error('no project specified'));
- return Promise.resolve([]);
- }
+ _getProjectBranchesSuggestions(input: string) {
+ if (!this.project) return Promise.reject(new Error('Missing project'));
if (input.startsWith('refs/heads/')) {
input = input.substring('refs/heads/'.length);
}
return this.restApiService
.getRepoBranches(input, this.project, SUGGESTIONS_LIMIT)
- .then((response: BranchInfo[] | undefined) => {
+ .then(response => {
if (!response) return [];
- const branches = [];
+ const branches: Array<{name: BranchName}> = [];
for (const branchInfo of response) {
- let branch;
- if (branchInfo.ref.startsWith('refs/heads/')) {
- branch = branchInfo.ref.substring('refs/heads/'.length);
- } else {
- branch = branchInfo.ref;
+ let name: string = branchInfo.ref;
+ if (name.startsWith('refs/heads/')) {
+ name = name.substring('refs/heads/'.length);
}
- branches.push({name: branch});
+ branches.push({name: name as BranchName});
}
return branches;
});
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-dialog/gr-confirm-cherrypick-dialog_test.js b/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-dialog/gr-confirm-cherrypick-dialog_test.js
index ff078f0..3df997f8 100644
--- a/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-dialog/gr-confirm-cherrypick-dialog_test.js
+++ b/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-dialog/gr-confirm-cherrypick-dialog_test.js
@@ -115,18 +115,19 @@
current_revision: 'a',
},
];
- setup(() => {
+ setup(async () => {
element.updateChanges(changes);
element._cherryPickType = CHERRY_PICK_TYPES.TOPIC;
- flush();
+ await flush();
});
test('cherry pick topic submit', async () => {
element.branch = 'master';
+ await flush();
const executeChangeActionStub = stubRestApi(
'executeChangeAction').returns(Promise.resolve([]));
MockInteractions.tap(element.shadowRoot.
- querySelector('gr-dialog').$.confirm);
+ querySelector('gr-dialog').confirmButton);
await flush();
const args = executeChangeActionStub.args[0];
assert.equal(args[0], 1);
@@ -137,26 +138,29 @@
assert.isTrue(args[4].allow_empty);
});
- test('deselecting a change removes it from being cherry picked', () => {
- const duplicateChangesStub = sinon.stub(element,
- 'containsDuplicateProject');
- element.branch = 'master';
- const executeChangeActionStub = stubRestApi(
- 'executeChangeAction').returns(Promise.resolve([]));
- const checkboxes = element.shadowRoot.querySelectorAll(
- 'input[type="checkbox"]');
- assert.equal(checkboxes.length, 2);
- assert.isTrue(checkboxes[0].checked);
- MockInteractions.tap(checkboxes[0]);
- MockInteractions.tap(element.shadowRoot.
- querySelector('gr-dialog').$.confirm);
- flush();
- assert.equal(executeChangeActionStub.callCount, 1);
- assert.isTrue(duplicateChangesStub.called);
- });
+ test('deselecting a change removes it from being cherry picked',
+ async () => {
+ const duplicateChangesStub = sinon.stub(element,
+ 'containsDuplicateProject');
+ element.branch = 'master';
+ await flush();
+ const executeChangeActionStub = stubRestApi(
+ 'executeChangeAction').returns(Promise.resolve([]));
+ const checkboxes = element.shadowRoot.querySelectorAll(
+ 'input[type="checkbox"]');
+ assert.equal(checkboxes.length, 2);
+ assert.isTrue(checkboxes[0].checked);
+ MockInteractions.tap(checkboxes[0]);
+ MockInteractions.tap(element.shadowRoot.
+ querySelector('gr-dialog').confirmButton);
+ await flush();
+ assert.equal(executeChangeActionStub.callCount, 1);
+ assert.isTrue(duplicateChangesStub.called);
+ });
- test('deselecting all change shows error message', () => {
+ test('deselecting all change shows error message', async () => {
element.branch = 'master';
+ await flush();
const executeChangeActionStub = stubRestApi(
'executeChangeAction').returns(Promise.resolve([]));
const checkboxes = element.shadowRoot.querySelectorAll(
@@ -165,8 +169,8 @@
MockInteractions.tap(checkboxes[0]);
MockInteractions.tap(checkboxes[1]);
MockInteractions.tap(element.shadowRoot.
- querySelector('gr-dialog').$.confirm);
- flush();
+ querySelector('gr-dialog').confirmButton);
+ await flush();
assert.equal(executeChangeActionStub.callCount, 0);
assert.equal(element.shadowRoot.querySelector('.error-message').innerText
, 'No change selected');
@@ -180,8 +184,8 @@
});
test('submit button is blocked while cherry picks is running', async () => {
- const confirmButton = element.shadowRoot.querySelector('gr-dialog').$
- .confirm;
+ const confirmButton = element.shadowRoot.querySelector('gr-dialog')
+ .confirmButton;
assert.isTrue(confirmButton.hasAttribute('disabled'));
element.branch = 'b';
await flush();
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-move-dialog/gr-confirm-move-dialog.ts b/polygerrit-ui/app/elements/change/gr-confirm-move-dialog/gr-confirm-move-dialog.ts
index c196706..8e6521d 100644
--- a/polygerrit-ui/app/elements/change/gr-confirm-move-dialog/gr-confirm-move-dialog.ts
+++ b/polygerrit-ui/app/elements/change/gr-confirm-move-dialog/gr-confirm-move-dialog.ts
@@ -21,15 +21,23 @@
import {htmlTemplate} from './gr-confirm-move-dialog_html';
import {KeyboardShortcutMixin} from '../../../mixins/keyboard-shortcut-mixin/keyboard-shortcut-mixin';
import {customElement, property} from '@polymer/decorators';
-import {RepoName, BranchName} from '../../../types/common';
-import {AutocompleteSuggestion} from '../../shared/gr-autocomplete/gr-autocomplete';
+import {BranchName, RepoName} from '../../../types/common';
import {appContext} from '../../../services/app-context';
+import {GrTypedAutocomplete} from '../../shared/gr-autocomplete/gr-autocomplete';
const SUGGESTIONS_LIMIT = 15;
// This avoids JSC_DYNAMIC_EXTENDS_WITHOUT_JSDOC closure compiler error.
const base = KeyboardShortcutMixin(PolymerElement);
+// This is used to make sure 'branch'
+// can be typed as BranchName.
+export interface GrConfirmMoveDialog {
+ $: {
+ branchInput: GrTypedAutocomplete<BranchName>;
+ };
+}
+
@customElement('gr-confirm-move-dialog')
export class GrConfirmMoveDialog extends base {
static get template() {
@@ -49,16 +57,16 @@
*/
@property({type: String})
- branch?: BranchName;
+ branch = '' as BranchName;
@property({type: String})
- message?: string;
+ message = '';
@property({type: String})
project?: RepoName;
@property({type: Object})
- _query: (input: string) => Promise<AutocompleteSuggestion[]>;
+ _query?: (input: string) => Promise<{name: BranchName}[]>;
get keyBindings() {
return {
@@ -95,9 +103,7 @@
);
}
- _getProjectBranchesSuggestions(
- input: string
- ): Promise<AutocompleteSuggestion[]> {
+ _getProjectBranchesSuggestions(input: string) {
if (!this.project) return Promise.reject(new Error('Missing project'));
if (input.startsWith('refs/heads/')) {
input = input.substring('refs/heads/'.length);
@@ -105,21 +111,15 @@
return this.restApiService
.getRepoBranches(input, this.project, SUGGESTIONS_LIMIT)
.then(response => {
- const branches: AutocompleteSuggestion[] = [];
- let branch;
- if (response) {
- response.forEach(value => {
- if (value.ref.startsWith('refs/heads/')) {
- branch = value.ref.substring('refs/heads/'.length);
- } else {
- branch = value.ref;
- }
- branches.push({
- name: branch,
- });
- });
+ if (!response) return [];
+ const branches: Array<{name: BranchName}> = [];
+ for (const branchInfo of response) {
+ let name: string = branchInfo.ref;
+ if (name.startsWith('refs/heads/')) {
+ name = name.substring('refs/heads/'.length);
+ }
+ branches.push({name: name as BranchName});
}
-
return branches;
});
}
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-move-dialog/gr-confirm-move-dialog_test.js b/polygerrit-ui/app/elements/change/gr-confirm-move-dialog/gr-confirm-move-dialog_test.ts
similarity index 75%
rename from polygerrit-ui/app/elements/change/gr-confirm-move-dialog/gr-confirm-move-dialog_test.js
rename to polygerrit-ui/app/elements/change/gr-confirm-move-dialog/gr-confirm-move-dialog_test.ts
index 10844f5..ea5d320 100644
--- a/polygerrit-ui/app/elements/change/gr-confirm-move-dialog/gr-confirm-move-dialog_test.js
+++ b/polygerrit-ui/app/elements/change/gr-confirm-move-dialog/gr-confirm-move-dialog_test.ts
@@ -15,35 +15,37 @@
* limitations under the License.
*/
-import '../../../test/common-test-setup-karma.js';
-import './gr-confirm-move-dialog.js';
-import {stubRestApi} from '../../../test/test-utils.js';
+import '../../../test/common-test-setup-karma';
+import './gr-confirm-move-dialog';
+import {GrConfirmMoveDialog} from './gr-confirm-move-dialog';
+import {stubRestApi} from '../../../test/test-utils';
+import {BranchName, GitRef, RepoName} from '../../../types/common';
const basicFixture = fixtureFromElement('gr-confirm-move-dialog');
suite('gr-confirm-move-dialog tests', () => {
- let element;
+ let element: GrConfirmMoveDialog;
setup(() => {
- stubRestApi('getRepoBranches').callsFake(input => {
+ stubRestApi('getRepoBranches').callsFake((input: string) => {
if (input.startsWith('test')) {
return Promise.resolve([
{
- ref: 'refs/heads/test-branch',
+ ref: 'refs/heads/test-branch' as GitRef,
revision: '67ebf73496383c6777035e374d2d664009e2aa5c',
can_delete: true,
},
]);
} else {
- return Promise.resolve(undefined);
+ return Promise.resolve([]);
}
});
element = basicFixture.instantiate();
- element.project = 'test-project';
+ element.project = 'test-repo' as RepoName;
});
test('with updated commit message', () => {
- element.branch = 'master';
+ element.branch = 'master' as BranchName;
const myNewMessage = 'updated commit message';
element.message = myNewMessage;
flush();
@@ -52,13 +54,15 @@
test('_getProjectBranchesSuggestions empty', async () => {
const branches = await element._getProjectBranchesSuggestions(
- 'nonexistent');
+ 'nonexistent'
+ );
assert.equal(branches.length, 0);
});
test('_getProjectBranchesSuggestions non-empty', async () => {
const branches = await element._getProjectBranchesSuggestions(
- 'test-branch');
+ 'test-branch'
+ );
assert.equal(branches.length, 1);
assert.equal(branches[0].name, 'test-branch');
});
@@ -68,4 +72,3 @@
assert.equal(branches.length, 0);
});
});
-
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-submit-dialog/gr-confirm-submit-dialog.ts b/polygerrit-ui/app/elements/change/gr-confirm-submit-dialog/gr-confirm-submit-dialog.ts
index c705319..9d371d3 100644
--- a/polygerrit-ui/app/elements/change/gr-confirm-submit-dialog/gr-confirm-submit-dialog.ts
+++ b/polygerrit-ui/app/elements/change/gr-confirm-submit-dialog/gr-confirm-submit-dialog.ts
@@ -58,7 +58,7 @@
@property({type: Boolean})
_initialised = false;
- static get styles() {
+ static override get styles() {
return [
sharedStyles,
fontStyles,
@@ -112,8 +112,8 @@
id="commentList"
.threads="${this._computeUnresolvedThreads(this.commentThreads)}"
.change="${this.change}"
- .change-num="${this.change?._number}"
- logged-in="true"
+ .changeNum="${this.change?._number}"
+ logged-in
hide-dropdown
>
</gr-thread-list>
@@ -154,7 +154,7 @@
`;
}
- render() {
+ override render() {
return html` <gr-dialog
id="dialog"
confirm-label="Continue"
diff --git a/polygerrit-ui/app/elements/change/gr-file-list-header/gr-file-list-header.ts b/polygerrit-ui/app/elements/change/gr-file-list-header/gr-file-list-header.ts
index bdc6a43..8aef3c0 100644
--- a/polygerrit-ui/app/elements/change/gr-file-list-header/gr-file-list-header.ts
+++ b/polygerrit-ui/app/elements/change/gr-file-list-header/gr-file-list-header.ts
@@ -44,7 +44,12 @@
import {DiffViewMode} from '../../../constants/constants';
import {GrButton} from '../../shared/gr-button/gr-button';
import {fireEvent} from '../../../utils/event-util';
-import {KeyboardShortcutMixin} from '../../../mixins/keyboard-shortcut-mixin/keyboard-shortcut-mixin';
+import {
+ KeyboardShortcutMixin,
+ Shortcut,
+ ShortcutSection,
+} from '../../../mixins/keyboard-shortcut-mixin/keyboard-shortcut-mixin';
+import {appContext} from '../../../services/app-context';
declare global {
interface HTMLElementTagNameMap {
@@ -144,6 +149,8 @@
@property({type: Object})
revisionInfo?: RevisionInfo;
+ private readonly shortcuts = appContext.shortcutsService;
+
setDiffViewMode(mode: DiffViewMode) {
this.$.modeSelect.setMode(mode);
}
@@ -217,4 +224,8 @@
}
return 'patchInfoOldPatchSet';
}
+
+ createTitle(shortcutName: Shortcut, section: ShortcutSection) {
+ return this.shortcuts.createTitle(shortcutName, section);
+ }
}
diff --git a/polygerrit-ui/app/elements/change/gr-file-list-header/gr-file-list-header_html.ts b/polygerrit-ui/app/elements/change/gr-file-list-header/gr-file-list-header_html.ts
index bceee27..5972393 100644
--- a/polygerrit-ui/app/elements/change/gr-file-list-header/gr-file-list-header_html.ts
+++ b/polygerrit-ui/app/elements/change/gr-file-list-header/gr-file-list-header_html.ts
@@ -93,9 +93,7 @@
}
.fileViewActions gr-button {
margin: 0;
- --gr-button: {
- padding: 2px 4px;
- }
+ --gr-button-padding: 2px 4px;
}
.editMode .hideOnEdit {
display: none;
@@ -180,50 +178,50 @@
hidden$="[[_computePrefsButtonHidden(diffPrefs, diffPrefsDisabled)]]"
hidden=""
>
- <gr-button
- link=""
- has-tooltip=""
- title="Diff preferences"
- class="prefsButton desktop"
- on-click="_handlePrefsTap"
- ><iron-icon icon="gr-icons:settings"></iron-icon
- ></gr-button>
+ <gr-tooltip-content has-tooltip title="Diff preferences">
+ <gr-button
+ link=""
+ class="prefsButton desktop"
+ on-click="_handlePrefsTap"
+ ><iron-icon icon="gr-icons:settings"></iron-icon
+ ></gr-button>
+ </gr-tooltip-content>
</span>
<span class="separator"></span>
</div>
<span class="downloadContainer desktop">
- <gr-button
- link=""
- class="download"
+ <gr-tooltip-content
+ has-tooltip
title="[[createTitle(Shortcut.OPEN_DOWNLOAD_DIALOG,
- ShortcutSection.ACTIONS)]]"
- has-tooltip=""
- on-click="_handleDownloadTap"
- >Download</gr-button
+ ShortcutSection.ACTIONS)]]"
>
+ <gr-button link="" class="download" on-click="_handleDownloadTap"
+ >Download</gr-button
+ >
+ </gr-tooltip-content>
</span>
<template
is="dom-if"
if="[[_fileListActionsVisible(shownFileCount, _maxFilesForBulkActions)]]"
>
- <gr-button
- id="expandBtn"
- link=""
+ <gr-tooltip-content
+ has-tooltip
title="[[createTitle(Shortcut.TOGGLE_ALL_INLINE_DIFFS,
- ShortcutSection.FILE_LIST)]]"
- has-tooltip=""
- on-click="_expandAllDiffs"
- >Expand All</gr-button
+ ShortcutSection.FILE_LIST)]]"
>
- <gr-button
- id="collapseBtn"
- link=""
- on-click="_collapseAllDiffs"
+ <gr-button id="expandBtn" link="" on-click="_expandAllDiffs"
+ >Expand All</gr-button
+ >
+ </gr-tooltip-content>
+ <gr-tooltip-content
+ has-tooltip
title="[[createTitle(Shortcut.TOGGLE_ALL_INLINE_DIFFS,
- ShortcutSection.FILE_LIST)]]"
- has-tooltip=""
- >Collapse All</gr-button
+ ShortcutSection.FILE_LIST)]]"
>
+ <gr-button id="collapseBtn" link="" on-click="_collapseAllDiffs"
+ >Collapse All</gr-button
+ >
+ </gr-tooltip-content>
</template>
<template
is="dom-if"
diff --git a/polygerrit-ui/app/elements/change/gr-file-list-header/gr-file-list-header_test.js b/polygerrit-ui/app/elements/change/gr-file-list-header/gr-file-list-header_test.js
index 08e5e3b..c90cfcc 100644
--- a/polygerrit-ui/app/elements/change/gr-file-list-header/gr-file-list-header_test.js
+++ b/polygerrit-ui/app/elements/change/gr-file-list-header/gr-file-list-header_test.js
@@ -90,26 +90,26 @@
assert.isFalse(computeSpy.lastCall.returnValue);
});
- test('fileViewActions are properly hidden', () => {
+ test('fileViewActions are properly hidden', async () => {
const actions = element.shadowRoot
.querySelector('.fileViewActions');
assert.equal(getComputedStyle(actions).display, 'none');
element.filesExpanded = FilesExpandedState.SOME;
- flush();
+ await flush();
assert.notEqual(getComputedStyle(actions).display, 'none');
element.filesExpanded = FilesExpandedState.ALL;
- flush();
+ await flush();
assert.notEqual(getComputedStyle(actions).display, 'none');
element.filesExpanded = FilesExpandedState.NONE;
- flush();
+ await flush();
assert.equal(getComputedStyle(actions).display, 'none');
});
- test('expand/collapse buttons are toggled correctly', () => {
+ test('expand/collapse buttons are toggled correctly', async () => {
// Only the expand button should be visible in the initial state when
// NO files are expanded.
element.shownFileCount = 10;
- flush();
+ await flush();
const expandBtn = element.shadowRoot.querySelector('#expandBtn');
const collapseBtn = element.shadowRoot.querySelector('#collapseBtn');
assert.notEqual(getComputedStyle(expandBtn).display, 'none');
@@ -118,19 +118,19 @@
// Both expand and collapse buttons should be visible when SOME files are
// expanded.
element.filesExpanded = FilesExpandedState.SOME;
- flush();
+ await flush();
assert.notEqual(getComputedStyle(expandBtn).display, 'none');
assert.notEqual(getComputedStyle(collapseBtn).display, 'none');
// Only the collapse button should be visible when ALL files are expanded.
element.filesExpanded = FilesExpandedState.ALL;
- flush();
+ await flush();
assert.equal(getComputedStyle(expandBtn).display, 'none');
assert.notEqual(getComputedStyle(collapseBtn).display, 'none');
// Only the expand button should be visible when NO files are expanded.
element.filesExpanded = FilesExpandedState.NONE;
- flush();
+ await flush();
assert.notEqual(getComputedStyle(expandBtn).display, 'none');
assert.equal(getComputedStyle(collapseBtn).display, 'none');
});
diff --git a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.ts b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.ts
index 8d0c203..b78c78f 100644
--- a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.ts
+++ b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.ts
@@ -14,6 +14,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
+import '../../../styles/gr-a11y-styles';
import '../../../styles/shared-styles';
import '../../diff/gr-diff-cursor/gr-diff-cursor';
import '../../diff/gr-diff-host/gr-diff-host';
@@ -49,8 +50,8 @@
} from '../../../constants/constants';
import {
descendedFromClass,
- getKeyboardEvent,
isShiftPressed,
+ modifierPressed,
toggleClass,
} from '../../../utils/dom-util';
import {
@@ -77,7 +78,7 @@
import {GrCursorManager} from '../../shared/gr-cursor-manager/gr-cursor-manager';
import {PolymerSpliceChange} from '@polymer/polymer/interfaces';
import {ChangeComments} from '../../diff/gr-comment-api/gr-comment-api';
-import {CustomKeyboardEvent} from '../../../types/events';
+import {IronKeyboardEvent} from '../../../types/events';
import {ParsedChangeInfo, PatchSetFile} from '../../../types/types';
import {Timing} from '../../../constants/reporting';
import {RevisionInfo} from '../../shared/revision-info/revision-info';
@@ -226,9 +227,6 @@
@property({type: Object, notify: true, observer: '_updateDiffPreferences'})
diffPrefs?: DiffPreferencesInfo;
- @property({type: Boolean})
- _showInlineDiffs?: boolean;
-
@property({type: Number, notify: true})
numFilesShown: number = DEFAULT_NUM_FILES_SHOWN;
@@ -358,14 +356,14 @@
private diffCursor = new GrDiffCursor();
+ private readonly shortcuts = appContext.shortcutsService;
+
constructor() {
super();
this.fileCursor.scrollMode = ScrollMode.KEEP_VISIBLE;
this.fileCursor.cursorTargetClass = 'selected';
this.fileCursor.focusOnMove = true;
- this.addEventListener('keydown', e =>
- this._scopedKeydownHandler(e as unknown as CustomKeyboardEvent)
- );
+ this.addEventListener('keydown', e => this._scopedKeydownHandler(e));
}
override connectedCallback() {
@@ -435,13 +433,8 @@
*
* Context: Issue 7277
*/
- _scopedKeydownHandler(e: CustomKeyboardEvent) {
- if (e.keyCode === 13) {
- // TODO(TS): e is not an instance of CustomKeyboardEvent.
- // However, to fix it we should fix keyboard-shortcut-mixin first
- // The keyboard-shortcut-mixin will be updated in a separate change
- this._handleOpenFile(e as unknown as CustomKeyboardEvent);
- }
+ _scopedKeydownHandler(e: KeyboardEvent) {
+ if (e.keyCode === 13) this.handleOpenFile(e);
}
reload() {
@@ -628,8 +621,6 @@
}
expandAllDiffs() {
- this._showInlineDiffs = true;
-
// Find the list of paths that are in the file list, but not in the
// expanded list.
const newFiles: PatchSetFile[] = [];
@@ -645,7 +636,6 @@
}
collapseAllDiffs() {
- this._showInlineDiffs = false;
this._expandedFiles = [];
this.filesExpanded = this._computeExpandedFiles(
this._expandedFiles.length,
@@ -891,8 +881,8 @@
return fileData;
}
- _handleLeftPane(e: CustomKeyboardEvent) {
- if (this.shouldSuppressKeyboardShortcut(e) || this._noDiffsExpanded()) {
+ _handleLeftPane(e: IronKeyboardEvent) {
+ if (this.shortcuts.shouldSuppress(e) || this._noDiffsExpanded()) {
return;
}
@@ -900,8 +890,8 @@
this.diffCursor.moveLeft();
}
- _handleRightPane(e: CustomKeyboardEvent) {
- if (this.shouldSuppressKeyboardShortcut(e) || this._noDiffsExpanded()) {
+ _handleRightPane(e: IronKeyboardEvent) {
+ if (this.shortcuts.shouldSuppress(e) || this._noDiffsExpanded()) {
return;
}
@@ -909,9 +899,9 @@
this.diffCursor.moveRight();
}
- _handleToggleInlineDiff(e: CustomKeyboardEvent) {
+ _handleToggleInlineDiff(e: IronKeyboardEvent) {
if (
- this.shouldSuppressKeyboardShortcut(e) ||
+ this.shortcuts.shouldSuppress(e) ||
this.modifierPressed(e) ||
e.detail?.keyboardEvent?.repeat ||
this.fileCursor.index === -1
@@ -923,11 +913,8 @@
this._toggleFileExpandedByIndex(this.fileCursor.index);
}
- _handleToggleAllInlineDiffs(e: CustomKeyboardEvent) {
- if (
- this.shouldSuppressKeyboardShortcut(e) ||
- e.detail?.keyboardEvent?.repeat
- ) {
+ _handleToggleAllInlineDiffs(e: IronKeyboardEvent) {
+ if (this.shortcuts.shouldSuppress(e) || e.detail?.keyboardEvent?.repeat) {
return;
}
@@ -935,8 +922,8 @@
this._toggleInlineDiffs();
}
- _handleToggleHideAllCommentThreads(e: CustomKeyboardEvent) {
- if (this.shouldSuppressKeyboardShortcut(e) || this.modifierPressed(e)) {
+ _handleToggleHideAllCommentThreads(e: IronKeyboardEvent) {
+ if (this.shortcuts.shouldSuppress(e) || this.modifierPressed(e)) {
return;
}
@@ -944,18 +931,18 @@
toggleClass(this, 'hideComments');
}
- _handleCursorNext(e: CustomKeyboardEvent) {
- if (this.shouldSuppressKeyboardShortcut(e) || this.modifierPressed(e)) {
+ _handleCursorNext(e: IronKeyboardEvent) {
+ if (this.shortcuts.shouldSuppress(e) || this.modifierPressed(e)) {
return;
}
- if (this._showInlineDiffs) {
+ if (this.filesExpanded === FilesExpandedState.ALL) {
e.preventDefault();
this.diffCursor.moveDown();
this._displayLine = true;
} else {
// Down key
- if (getKeyboardEvent(e).keyCode === 40) {
+ if (e.detail.keyboardEvent.keyCode === 40) {
return;
}
e.preventDefault();
@@ -964,18 +951,18 @@
}
}
- _handleCursorPrev(e: CustomKeyboardEvent) {
- if (this.shouldSuppressKeyboardShortcut(e) || this.modifierPressed(e)) {
+ _handleCursorPrev(e: IronKeyboardEvent) {
+ if (this.shortcuts.shouldSuppress(e) || this.modifierPressed(e)) {
return;
}
- if (this._showInlineDiffs) {
+ if (this.filesExpanded === FilesExpandedState.ALL) {
e.preventDefault();
this.diffCursor.moveUp();
this._displayLine = true;
} else {
// Up key
- if (getKeyboardEvent(e).keyCode === 38) {
+ if (e.detail.keyboardEvent.keyCode === 38) {
return;
}
e.preventDefault();
@@ -984,8 +971,8 @@
}
}
- _handleNewComment(e: CustomKeyboardEvent) {
- if (this.shouldSuppressKeyboardShortcut(e) || this.modifierPressed(e)) {
+ _handleNewComment(e: IronKeyboardEvent) {
+ if (this.shortcuts.shouldSuppress(e) || this.modifierPressed(e)) {
return;
}
e.preventDefault();
@@ -993,9 +980,9 @@
this.diffCursor.createCommentInPlace();
}
- _handleOpenLastFile(e: CustomKeyboardEvent) {
+ _handleOpenLastFile(e: IronKeyboardEvent) {
// Check for meta key to avoid overriding native chrome shortcut.
- if (this.shouldSuppressKeyboardShortcut(e) || getKeyboardEvent(e).metaKey) {
+ if (this.shortcuts.shouldSuppress(e) || e.detail.keyboardEvent.metaKey) {
return;
}
@@ -1003,9 +990,9 @@
this._openSelectedFile(this._files.length - 1);
}
- _handleOpenFirstFile(e: CustomKeyboardEvent) {
+ _handleOpenFirstFile(e: IronKeyboardEvent) {
// Check for meta key to avoid overriding native chrome shortcut.
- if (this.shouldSuppressKeyboardShortcut(e) || getKeyboardEvent(e).metaKey) {
+ if (this.shortcuts.shouldSuppress(e) || e.detail.keyboardEvent.metaKey) {
return;
}
@@ -1013,13 +1000,18 @@
this._openSelectedFile(0);
}
- _handleOpenFile(e: CustomKeyboardEvent) {
- if (this.shouldSuppressKeyboardShortcut(e) || this.modifierPressed(e)) {
+ _handleOpenFile(e: IronKeyboardEvent) {
+ if (this.modifierPressed(e)) return;
+ this.handleOpenFile(e.detail.keyboardEvent);
+ }
+
+ handleOpenFile(e: KeyboardEvent) {
+ if (this.shortcuts.shouldSuppress(e) || modifierPressed(e)) {
return;
}
e.preventDefault();
- if (this._showInlineDiffs) {
+ if (this.filesExpanded === FilesExpandedState.ALL) {
this._openCursorFile();
return;
}
@@ -1027,9 +1019,9 @@
this._openSelectedFile();
}
- _handleNextChunk(e: CustomKeyboardEvent) {
+ _handleNextChunk(e: IronKeyboardEvent) {
if (
- this.shouldSuppressKeyboardShortcut(e) ||
+ this.shortcuts.shouldSuppress(e) ||
(this.modifierPressed(e) && !isShiftPressed(e)) ||
this._noDiffsExpanded()
) {
@@ -1044,9 +1036,9 @@
}
}
- _handlePrevChunk(e: CustomKeyboardEvent) {
+ _handlePrevChunk(e: IronKeyboardEvent) {
if (
- this.shouldSuppressKeyboardShortcut(e) ||
+ this.shortcuts.shouldSuppress(e) ||
(this.modifierPressed(e) && !isShiftPressed(e)) ||
this._noDiffsExpanded()
) {
@@ -1061,8 +1053,8 @@
}
}
- _handleToggleFileReviewed(e: CustomKeyboardEvent) {
- if (this.shouldSuppressKeyboardShortcut(e) || this.modifierPressed(e)) {
+ _handleToggleFileReviewed(e: IronKeyboardEvent) {
+ if (this.shortcuts.shouldSuppress(e) || this.modifierPressed(e)) {
return;
}
@@ -1073,8 +1065,8 @@
this._reviewFile(this._files[this.fileCursor.index].__path);
}
- _handleToggleLeftPane(e: CustomKeyboardEvent) {
- if (this.shouldSuppressKeyboardShortcut(e)) {
+ _handleToggleLeftPane(e: IronKeyboardEvent) {
+ if (this.shortcuts.shouldSuppress(e)) {
return;
}
@@ -1085,7 +1077,7 @@
}
_toggleInlineDiffs() {
- if (this._showInlineDiffs) {
+ if (this.filesExpanded === FilesExpandedState.ALL) {
this.collapseAllDiffs();
} else {
this.expandAllDiffs();
@@ -1550,8 +1542,8 @@
return undefined;
}
- _handleEscKey(e: CustomKeyboardEvent) {
- if (this.shouldSuppressKeyboardShortcut(e) || this.modifierPressed(e)) {
+ _handleEscKey(e: IronKeyboardEvent) {
+ if (this.shortcuts.shouldSuppress(e) || this.modifierPressed(e)) {
return;
}
e.preventDefault();
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 4d04744..f7be36b 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
@@ -17,6 +17,9 @@
import {html} from '@polymer/polymer/lib/utils/html-tag';
export const htmlTemplate = html`
+ <style include="gr-a11y-styles">
+ /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
+ </style>
<style include="shared-styles">
:host {
display: block;
@@ -253,9 +256,7 @@
display: inline-block;
visibility: hidden;
vertical-align: bottom;
- --gr-button: {
- padding: 0px;
- }
+ --gr-button-padding: 0px;
}
.row:focus-within gr-copy-clipboard,
.row:hover gr-copy-clipboard {
diff --git a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_test.js b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_test.js
index 5def25a..f4064da 100644
--- a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_test.js
+++ b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_test.js
@@ -17,6 +17,7 @@
import '../../../test/common-test-setup-karma.js';
import '../../diff/gr-comment-api/gr-comment-api.js';
+import '../../shared/gr-date-formatter/gr-date-formatter.js';
import {getMockDiffResponse} from '../../../test/mocks/diff-response.js';
import './gr-file-list.js';
import {createCommentApiMockWithTemplateElement} from '../../../test/mocks/comment-api.js';
@@ -26,7 +27,6 @@
import {runA11yAudit} from '../../../test/a11y-test-utils.js';
import {html} from '@polymer/polymer/lib/utils/html-tag.js';
import {
- TestKeyboardShortcutBinder,
stubRestApi,
spyRestApi,
listenOnce,
@@ -34,7 +34,6 @@
query,
} from '../../../test/test-utils.js';
import {EditPatchSetNum} from '../../../types/common.js';
-import {Shortcut} from '../../../mixins/keyboard-shortcut-mixin/keyboard-shortcut-mixin.js';
import {createCommentThreads} from '../../../utils/comment-util.js';
import {
createChange,
@@ -67,30 +66,6 @@
let saveStub;
- suiteSetup(() => {
- const kb = TestKeyboardShortcutBinder.push();
- kb.bindShortcut(Shortcut.LEFT_PANE, 'shift+left');
- kb.bindShortcut(Shortcut.RIGHT_PANE, 'shift+right');
- kb.bindShortcut(Shortcut.TOGGLE_INLINE_DIFF, 'i:keyup');
- kb.bindShortcut(Shortcut.TOGGLE_ALL_INLINE_DIFFS, 'shift+i:keyup');
- kb.bindShortcut(Shortcut.CURSOR_NEXT_FILE, 'j', 'down');
- kb.bindShortcut(Shortcut.CURSOR_PREV_FILE, 'k', 'up');
- kb.bindShortcut(Shortcut.NEXT_LINE, 'j', 'down');
- kb.bindShortcut(Shortcut.PREV_LINE, 'k', 'up');
- kb.bindShortcut(Shortcut.NEW_COMMENT, 'c');
- kb.bindShortcut(Shortcut.OPEN_LAST_FILE, '[');
- kb.bindShortcut(Shortcut.OPEN_FIRST_FILE, ']');
- kb.bindShortcut(Shortcut.OPEN_FILE, 'o');
- kb.bindShortcut(Shortcut.NEXT_CHUNK, 'n');
- kb.bindShortcut(Shortcut.PREV_CHUNK, 'p');
- kb.bindShortcut(Shortcut.TOGGLE_FILE_REVIEWED, 'r');
- kb.bindShortcut(Shortcut.TOGGLE_LEFT_PANE, 'shift+a');
- });
-
- suiteTeardown(() => {
- TestKeyboardShortcutBinder.pop();
- });
-
suite('basic tests', () => {
setup(async () => {
stubRestApi('getDiffComments').returns(Promise.resolve({}));
@@ -565,35 +540,36 @@
assert.equal(element.diffs.length, 0);
assert.equal(element._expandedFiles.length, 0);
- MockInteractions.keyUpOn(element, 73, null, 'i');
+ MockInteractions.pressAndReleaseKeyOn(element, 73, null, 'i');
flush();
assert.equal(element.diffs.length, 1);
assert.equal(element.diffs[0].path, paths[0]);
assert.equal(element._expandedFiles.length, 1);
assert.equal(element._expandedFiles[0].path, paths[0]);
- MockInteractions.keyUpOn(element, 73, null, 'i');
+ MockInteractions.pressAndReleaseKeyOn(element, 73, null, 'i');
flush();
assert.equal(element.diffs.length, 0);
assert.equal(element._expandedFiles.length, 0);
element.fileCursor.setCursorAtIndex(1);
- MockInteractions.keyUpOn(element, 73, null, 'i');
+ MockInteractions.pressAndReleaseKeyOn(element, 73, null, 'i');
flush();
assert.equal(element.diffs.length, 1);
assert.equal(element.diffs[0].path, paths[1]);
assert.equal(element._expandedFiles.length, 1);
assert.equal(element._expandedFiles[0].path, paths[1]);
- MockInteractions.keyUpOn(element, 73, 'shift', 'i');
+ MockInteractions.pressAndReleaseKeyOn(element, 73, 'shift', 'i');
flush();
assert.equal(element.diffs.length, paths.length);
assert.equal(element._expandedFiles.length, paths.length);
for (const diff of element.diffs) {
assert.isTrue(element._expandedFiles.some(f => f.path === diff.path));
}
-
- MockInteractions.keyUpOn(element, 73, 'shift', 'i');
+ // since _expandedFilesChanged is stubbed
+ element.filesExpanded = FilesExpandedState.ALL;
+ MockInteractions.pressAndReleaseKeyOn(element, 73, 'shift', 'i');
flush();
assert.equal(element.diffs.length, 0);
assert.equal(element._expandedFiles.length, 0);
@@ -608,12 +584,12 @@
assert.equal(getNumReviewed(), 0);
// Press the review key to toggle it (set the flag).
- MockInteractions.pressAndReleaseKeyOn(element, 82, null, 'r');
+ MockInteractions.keyUpOn(element, 82, null, 'r');
flush();
assert.equal(getNumReviewed(), 1);
// Press the review key to toggle it (clear the flag).
- MockInteractions.pressAndReleaseKeyOn(element, 82, null, 'r');
+ MockInteractions.keyUpOn(element, 82, null, 'r');
assert.equal(getNumReviewed(), 0);
});
@@ -621,22 +597,23 @@
let interact;
setup(() => {
- sinon.stub(element, 'shouldSuppressKeyboardShortcut')
- .returns(false);
sinon.stub(element, 'modifierPressed').returns(false);
const openCursorStub = sinon.stub(element, '_openCursorFile');
const openSelectedStub = sinon.stub(element, '_openSelectedFile');
const expandStub = sinon.stub(element, '_toggleFileExpanded');
- interact = function(opt_payload) {
+ interact = function() {
openCursorStub.reset();
openSelectedStub.reset();
expandStub.reset();
- const e = new CustomEvent('fake-keyboard-event', opt_payload);
- sinon.stub(e, 'preventDefault');
+ const keyboardEvent = new KeyboardEvent('keydown');
+ const e = new CustomEvent('keydown', {
+ detail: {keyboardEvent, key: 'x'},
+ });
+ sinon.stub(keyboardEvent, 'preventDefault');
element._handleOpenFile(e);
- assert.isTrue(e.preventDefault.called);
+ assert.isTrue(keyboardEvent.preventDefault.called);
const result = {};
if (openCursorStub.called) {
result.opened_cursor = true;
@@ -652,17 +629,17 @@
});
test('open from selected file', () => {
- element._showInlineDiffs = false;
+ element.filesExpanded = FilesExpandedState.NONE;
assert.deepEqual(interact(), {opened_selected: true});
});
test('open from diff cursor', () => {
- element._showInlineDiffs = true;
+ element.filesExpanded = FilesExpandedState.ALL;
assert.deepEqual(interact(), {opened_cursor: true});
});
test('expand when user prefers', () => {
- element._showInlineDiffs = false;
+ element.filesExpanded = FilesExpandedState.NONE;
assert.deepEqual(interact(), {opened_selected: true});
element._userPrefs = {};
assert.deepEqual(interact(), {opened_selected: true});
@@ -928,14 +905,14 @@
element._filesByPath = {[path]: {}};
element.expandAllDiffs();
flush();
- assert.isTrue(element._showInlineDiffs);
+ assert.equal(element.filesExpanded, FilesExpandedState.ALL);
assert.isTrue(reInitStub.calledOnce);
assert.equal(collapseStub.lastCall.args[0].length, 0);
element.collapseAllDiffs();
flush();
assert.equal(element._expandedFiles.length, 0);
- assert.isFalse(element._showInlineDiffs);
+ assert.equal(element.filesExpanded, FilesExpandedState.NONE);
assert.isTrue(cursorUpdateStub.calledOnce);
assert.equal(collapseStub.lastCall.args[0].length, 1);
});
@@ -1572,7 +1549,7 @@
});
test('cursor with individually opened files', async () => {
- MockInteractions.keyUpOn(element, 73, null, 'i');
+ MockInteractions.pressAndReleaseKeyOn(element, 73, null, 'i');
await flush();
let diffs = await renderAndGetNewDiffs(0);
const diffStops = diffs[0].getCursorStops();
@@ -1599,7 +1576,7 @@
// The file cursor is now at 1.
assert.equal(element.fileCursor.index, 1);
- MockInteractions.keyUpOn(element, 73, null, 'i');
+ MockInteractions.pressAndReleaseKeyOn(element, 73, null, 'i');
await flush();
diffs = await renderAndGetNewDiffs(1);
@@ -1614,7 +1591,7 @@
});
test('cursor with toggle all files', async () => {
- MockInteractions.keyUpOn(element, 73, 'shift', 'i');
+ MockInteractions.pressAndReleaseKeyOn(element, 73, 'shift', 'i');
await flush();
const diffs = await renderAndGetNewDiffs(0);
@@ -1661,7 +1638,7 @@
});
test('n key with some files expanded and no shift key', async () => {
- MockInteractions.keyUpOn(fileRows[0], 73, null, 'i');
+ MockInteractions.pressAndReleaseKeyOn(fileRows[0], 73, null, 'i');
await flush();
// Handle N key should return before calling diff cursor functions.
@@ -1675,7 +1652,7 @@
});
test('n key with some files expanded and shift key', async () => {
- MockInteractions.keyUpOn(fileRows[0], 73, null, 'i');
+ MockInteractions.pressAndReleaseKeyOn(fileRows[0], 73, null, 'i');
await flush();
assert.equal(nextChunkStub.callCount, 0);
@@ -1689,7 +1666,7 @@
});
test('n key without all files expanded and shift key', async () => {
- MockInteractions.keyUpOn(fileRows[0], 73, 'shift', 'i');
+ MockInteractions.pressAndReleaseKeyOn(fileRows[0], 73, 'shift', 'i');
await flush();
MockInteractions.pressAndReleaseKeyOn(element, 78, null, 'n');
@@ -1698,11 +1675,11 @@
// This is also called in diffCursor.moveToFirstChunk.
assert.equal(nextChunkStub.callCount, 1);
- assert.isTrue(element._showInlineDiffs);
+ assert.equal(element.filesExpanded, FilesExpandedState.ALL);
});
test('n key without all files expanded and no shift key', async () => {
- MockInteractions.keyUpOn(fileRows[0], 73, 'shift', 'i');
+ MockInteractions.pressAndReleaseKeyOn(fileRows[0], 73, 'shift', 'i');
await flush();
MockInteractions.pressAndReleaseKeyOn(element, 78, 'shift', 'n');
@@ -1711,7 +1688,7 @@
// This is also called in diffCursor.moveToFirstChunk.
assert.equal(nextChunkStub.callCount, 0);
- assert.isTrue(element._showInlineDiffs);
+ assert.equal(element.filesExpanded, FilesExpandedState.ALL);
});
});
@@ -1731,12 +1708,13 @@
});
test('_displayLine', () => {
- sinon.stub(element, 'shouldSuppressKeyboardShortcut')
- .callsFake(() => false);
sinon.stub(element, 'modifierPressed')
.callsFake(() => false);
- element._showInlineDiffs = true;
- const mockEvent = {preventDefault() {}};
+ element.filesExpanded = FilesExpandedState.ALL;
+ const mockEvent = {
+ preventDefault() {},
+ composedPath() { return []; },
+ };
element._displayLine = false;
element._handleCursorNext(mockEvent);
@@ -1757,13 +1735,13 @@
const saveReviewStub = sinon.stub(element, '_saveReviewedState');
element.editMode = false;
- MockInteractions.pressAndReleaseKeyOn(element, 82, null, 'r');
+ MockInteractions.keyUpOn(element, 82, null, 'r');
assert.isTrue(saveReviewStub.calledOnce);
element.editMode = true;
await flush();
- MockInteractions.pressAndReleaseKeyOn(element, 82, null, 'r');
+ MockInteractions.keyUpOn(element, 82, null, 'r');
assert.isTrue(saveReviewStub.calledOnce);
});
diff --git a/polygerrit-ui/app/elements/change/gr-label-score-row/gr-label-score-row_html.ts b/polygerrit-ui/app/elements/change/gr-label-score-row/gr-label-score-row_html.ts
index f9d21d9..0af2f36 100644
--- a/polygerrit-ui/app/elements/change/gr-label-score-row/gr-label-score-row_html.ts
+++ b/polygerrit-ui/app/elements/change/gr-label-score-row/gr-label-score-row_html.ts
@@ -53,25 +53,27 @@
);
padding: 0 var(--spacing-m);
}
- gr-button.iron-selected[vote='max'] {
+ gr-tooltip-content.iron-selected > gr-button[vote='max'] {
--button-background-color: var(--vote-color-approved);
}
- gr-button.iron-selected[vote='positive'] {
+ gr-tooltip-content.iron-selected > gr-buttonvote='positive'] {
--button-background-color: var(--vote-color-recommended);
}
- gr-button.iron-selected[vote='min'] {
+ gr-tooltip-content.iron-selected > gr-button[vote='min'] {
--button-background-color: var(--vote-color-rejected);
}
- gr-button.iron-selected[vote='negative'] {
+ gr-tooltip-content.iron-selected > gr-button[vote='negative'] {
--button-background-color: var(--vote-color-disliked);
}
- gr-button.iron-selected[vote='neutral'] {
+ gr-tooltip-content.iron-selected > gr-button[vote='neutral'] {
--button-background-color: var(--vote-color-neutral);
}
- gr-button.iron-selected[vote='positive']::part(paper-button) {
+ gr-tooltip-content.iron-selected
+ > gr-button[vote='positive']::part(paper-button) {
border-color: var(--vote-outline-recommended);
}
- gr-button.iron-selected[vote='negative']::part(paper-button) {
+ gr-tooltip-content.iron-selected
+ > gr-button[vote='negative']::part(paper-button) {
border-color: var(--vote-outline-disliked);
}
.placeholder {
@@ -116,17 +118,20 @@
aria-labelledby="labelName"
>
<template is="dom-repeat" items="[[_items]]" as="value">
- <gr-button
- role="radio"
- vote$="[[_computeVoteAttribute(value, index, _items.length)]]"
- vote-chip
- has-tooltip=""
+ <gr-tooltip-content
+ has-tooltip
+ title$="[[_computeLabelValueTitle(labels, label.name, value)]]"
data-name$="[[label.name]]"
data-value$="[[value]]"
- title$="[[_computeLabelValueTitle(labels, label.name, value)]]"
>
- [[value]]</gr-button
- >
+ <gr-button
+ role="radio"
+ vote$="[[_computeVoteAttribute(value, index, _items.length)]]"
+ voteChip
+ >
+ [[value]]
+ </gr-button>
+ </gr-tooltip-content>
</template>
</iron-selector>
<template
diff --git a/polygerrit-ui/app/elements/change/gr-label-score-row/gr-label-score-row_test.js b/polygerrit-ui/app/elements/change/gr-label-score-row/gr-label-score-row_test.js
index e7ff236..34e959b 100644
--- a/polygerrit-ui/app/elements/change/gr-label-score-row/gr-label-score-row_test.js
+++ b/polygerrit-ui/app/elements/change/gr-label-score-row/gr-label-score-row_test.js
@@ -97,14 +97,14 @@
}
}
- test('label picker', () => {
+ test('label picker', async () => {
const labelsChangedHandler = sinon.stub();
element.addEventListener('labels-changed', labelsChangedHandler);
assert.ok(element.$.labelSelector);
MockInteractions.tap(element.shadowRoot
.querySelector(
- 'gr-button[data-value="-1"]'));
- flush();
+ 'gr-tooltip-content[data-value="-1"] > gr-button'));
+ await flush();
assert.strictEqual(element.selectedValue, '-1');
assert.strictEqual(element.selectedItem
.textContent.trim(), '-1');
@@ -160,26 +160,6 @@
checkAriaCheckedValid();
});
- test('do not display tooltips on touch devices', () => {
- const verifiedBtn = element.shadowRoot
- .querySelector(
- 'iron-selector > gr-button[data-value="-1"]');
-
- // On touch devices, tooltips should not be shown.
- verifiedBtn._isTouchDevice = true;
- verifiedBtn._handleShowTooltip();
- assert.isNotOk(verifiedBtn._tooltip);
- verifiedBtn._handleHideTooltip();
- assert.isNotOk(verifiedBtn._tooltip);
-
- // On other devices, tooltips should be shown.
- verifiedBtn._isTouchDevice = false;
- verifiedBtn._handleShowTooltip();
- assert.isOk(verifiedBtn._tooltip);
- verifiedBtn._handleHideTooltip();
- assert.isNotOk(verifiedBtn._tooltip);
- });
-
test('_computeLabelValue', () => {
assert.strictEqual(element._computeLabelValue(element.labels,
element.permittedLabels,
@@ -209,7 +189,7 @@
'Code-Review'), []);
});
- test('changes in label score are reflected in the DOM', () => {
+ test('changes in label score are reflected in the DOM', async () => {
element.labels = {
'Code-Review': {
values: {
@@ -232,9 +212,10 @@
default_value: 0,
},
};
+ await flush();
const selector = element.$.labelSelector;
element.set('label', {name: 'Verified', value: ' 0'});
- flush();
+ await flush();
assert.strictEqual(selector.selected, ' 0');
assert.strictEqual(
element.$.selectedValueLabel.textContent.trim(), 'No score');
diff --git a/polygerrit-ui/app/elements/change/gr-label-scores/gr-label-scores_test.ts b/polygerrit-ui/app/elements/change/gr-label-scores/gr-label-scores_test.ts
index cfecf91..f529464 100644
--- a/polygerrit-ui/app/elements/change/gr-label-scores/gr-label-scores_test.ts
+++ b/polygerrit-ui/app/elements/change/gr-label-scores/gr-label-scores_test.ts
@@ -85,7 +85,7 @@
await flush();
});
- test('get and set label scores', () => {
+ test('get and set label scores', async () => {
for (const label of Object.keys(element.permittedLabels!)) {
const row = queryAndAssert<GrLabelScoreRow>(
element,
@@ -93,6 +93,7 @@
);
row.setSelectedValue('-1');
}
+ await flush();
assert.deepEqual(element.getLabelValues(), {
'Code-Review': -1,
Verified: -1,
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 b4e85a9..95e4301 100644
--- a/polygerrit-ui/app/elements/change/gr-message/gr-message.ts
+++ b/polygerrit-ui/app/elements/change/gr-message/gr-message.ts
@@ -209,7 +209,7 @@
this.addEventListener('click', e => this._handleClick(e));
}
- connectedCallback() {
+ override connectedCallback() {
super.connectedCallback();
this.restApiService.getConfig().then(config => {
this.config = config;
diff --git a/polygerrit-ui/app/elements/change/gr-message/gr-message_html.ts b/polygerrit-ui/app/elements/change/gr-message/gr-message_html.ts
index ddbd22e..7f3e9de 100644
--- a/polygerrit-ui/app/elements/change/gr-message/gr-message_html.ts
+++ b/polygerrit-ui/app/elements/change/gr-message/gr-message_html.ts
@@ -132,7 +132,7 @@
}
.dateContainer .patchsetDiffButton {
margin-right: var(--spacing-m);
- --padding: 0 var(--spacing-m);
+ --gr-button-padding: 0 var(--spacing-m);
}
span.date {
color: var(--deemphasized-text-color);
@@ -325,8 +325,8 @@
<template is="dom-if" if="[[!message.id]]">
<span class="date">
<gr-date-formatter
- has-tooltip=""
- show-date-and-time=""
+ withTooltip
+ showDateAndTime
date-str="[[message.date]]"
></gr-date-formatter>
</span>
@@ -334,8 +334,8 @@
<template is="dom-if" if="[[message.id]]">
<span class="date" on-click="_handleAnchorClick">
<gr-date-formatter
- has-tooltip=""
- show-date-and-time=""
+ withTooltip
+ showDateAndTime
date-str="[[message.date]]"
></gr-date-formatter>
</span>
diff --git a/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list.ts b/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list.ts
index fdd7c79..cd79bba 100644
--- a/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list.ts
+++ b/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list.ts
@@ -257,6 +257,8 @@
private readonly reporting = appContext.reportingService;
+ private readonly shortcuts = appContext.shortcutsService;
+
scrollToMessage(messageID: string) {
const selector = `[data-message-id="${messageID}"]`;
const el = this.shadowRoot!.querySelector(selector) as
@@ -384,13 +386,13 @@
_computeExpandAllTitle(_expandAllState?: string) {
if (_expandAllState === ExpandAllState.COLLAPSE_ALL) {
- return this.createTitle(
+ return this.shortcuts.createTitle(
Shortcut.COLLAPSE_ALL_MESSAGES,
ShortcutSection.ACTIONS
);
}
if (_expandAllState === ExpandAllState.EXPAND_ALL) {
- return this.createTitle(
+ return this.shortcuts.createTitle(
Shortcut.EXPAND_ALL_MESSAGES,
ShortcutSection.ACTIONS
);
diff --git a/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-change.ts b/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-change.ts
index d34600e..e4703df 100644
--- a/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-change.ts
+++ b/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-change.ts
@@ -24,6 +24,7 @@
} from '../../../types/common';
import {ChangeStatus} from '../../../constants/constants';
import {isChangeInfo} from '../../../utils/change-util';
+import {ifDefined} from 'lit/directives/if-defined';
@customElement('gr-related-change')
export class GrRelatedChange extends LitElement {
@@ -109,7 +110,7 @@
const linkClass = this._computeLinkClass(change);
return html`
<div class="changeContainer">
- <a href="${this.href}" class="${linkClass}"><slot></slot></a>
+ <a href="${ifDefined(this.href)}" class="${linkClass}"><slot></slot></a>
${this.showSubmittableCheck
? html`<span
tabindex="-1"
diff --git a/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-changes-list.ts b/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-changes-list.ts
index 74f20f2..bce4024 100644
--- a/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-changes-list.ts
+++ b/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-changes-list.ts
@@ -40,6 +40,7 @@
isChangeInfo,
} from '../../../utils/change-util';
import {Interaction} from '../../../constants/reporting';
+import {fontStyles} from '../../../styles/gr-font-styles';
/** What is the maximum number of shown changes in collapsed list? */
const DEFALT_NUM_CHANGES_WHEN_COLLAPSED = 3;
@@ -686,9 +687,9 @@
static override get styles() {
return [
sharedStyles,
+ fontStyles,
css`
.title {
- font-weight: var(--font-weight-bold);
color: var(--deemphasized-text-color);
padding-left: var(--metadata-horizontal-padding);
}
@@ -720,7 +721,7 @@
}
override render() {
- const title = html`<h4 class="title">${this.title}</h4>`;
+ const title = html`<h3 class="title heading-3">${this.title}</h3>`;
const collapsible = this.length > this.numChangesWhenCollapsed;
this.collapsed = !this.showAll && collapsible;
diff --git a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog-it_test.js b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog-it_test.js
index 933cb82..b8c9319 100644
--- a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog-it_test.js
+++ b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog-it_test.js
@@ -127,7 +127,7 @@
const labelScoreRows = element.getLabelScores().shadowRoot
.querySelector('gr-label-score-row[name="Code-Review"]');
const selectedBtn = labelScoreRows.shadowRoot
- .querySelector('gr-button[data-value="+1"].iron-selected');
+ .querySelector('gr-tooltip-content[data-value="+1"] > gr-button');
assert.isOk(selectedBtn);
});
});
diff --git a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_html.ts b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_html.ts
index b571985..0beb91c 100644
--- a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_html.ts
+++ b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_html.ts
@@ -177,7 +177,7 @@
}
.attention .edit-attention-button {
vertical-align: top;
- --padding: 0px 4px;
+ --gr-button-padding: 0px 4px;
}
.attention .edit-attention-button iron-icon {
color: inherit;
@@ -441,23 +441,26 @@
></gr-account-label>
</template>
</template>
- <gr-button
- class="edit-attention-button"
- on-click="_handleAttentionModify"
- disabled="[[_sendDisabled]]"
- link=""
- position-below=""
- data-label="Edit"
- data-action-type="change"
- data-action-key="edit"
- has-tooltip=""
+ <gr-tooltip-content
+ has-tooltip
title="[[_computeAttentionButtonTitle(_sendDisabled)]]"
- role="button"
- tabindex="0"
>
- <iron-icon icon="gr-icons:edit"></iron-icon>
- Modify
- </gr-button>
+ <gr-button
+ class="edit-attention-button"
+ on-click="_handleAttentionModify"
+ disabled="[[_sendDisabled]]"
+ link=""
+ position-below=""
+ data-label="Edit"
+ data-action-type="change"
+ data-action-key="edit"
+ role="button"
+ tabindex="0"
+ >
+ <iron-icon icon="gr-icons:edit"></iron-icon>
+ Modify
+ </gr-button>
+ </gr-tooltip-content>
</div>
<div>
<a
@@ -612,26 +615,32 @@
<!-- Use 'Send' here as the change may only about reviewers / ccs
and when this button is visible, the next button will always
be 'Start review' -->
- <gr-button
- link=""
- disabled="[[_isState(knownLatestState, 'not-latest')]]"
- class="action save"
+ <gr-tooltip-content
has-tooltip=""
- title="[[_saveTooltip]]"
- on-click="_saveClickHandler"
- >Send As WIP</gr-button
+ title$="[[_saveTooltip]]"
>
+ <gr-button
+ link=""
+ disabled="[[_isState(knownLatestState, 'not-latest')]]"
+ class="action save"
+ on-click="_saveClickHandler"
+ >Send As WIP</gr-button
+ >
+ </gr-tooltip-content>
</template>
- <gr-button
- id="sendButton"
- primary=""
- disabled="[[_sendDisabled]]"
- class="action send"
+ <gr-tooltip-content
has-tooltip=""
title$="[[_computeSendButtonTooltip(canBeStarted, _commentEditing)]]"
- on-click="_sendTapHandler"
- >[[_sendButtonLabel]]</gr-button
>
+ <gr-button
+ id="sendButton"
+ primary=""
+ disabled="[[_sendDisabled]]"
+ class="action send"
+ on-click="_sendTapHandler"
+ >[[_sendButtonLabel]]
+ </gr-button>
+ </gr-tooltip-content>
</div>
</section>
</div>
diff --git a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_test.ts b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_test.ts
index e3bbd9e..e57ffc7 100644
--- a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_test.ts
+++ b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_test.ts
@@ -116,7 +116,7 @@
return {id: `${lastId++}` as GroupId};
};
- setup(() => {
+ setup(async () => {
changeNum = 42 as NumericChangeId;
patchNum = 1 as PatchSetNum;
@@ -168,7 +168,7 @@
// .returns(Promise.resolve({isLatest: true}));
// Allow the elements created by dom-repeat to be stamped.
- flush();
+ await flush();
});
function stubSaveReview(
@@ -216,6 +216,7 @@
// which the dom-repeat elements are stamped.
await flush();
tap(queryAndAssert(element, '.send'));
+ await flush();
const review = await saveReviewPromise;
assert.deepEqual(review, {
@@ -1063,6 +1064,7 @@
const label = 'Verified';
const value = '+1';
element.setLabelValue(label, value);
+ await flush();
const labels = (
queryAndAssert(element, '#labelScores') as GrLabelScores
diff --git a/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list_html.ts b/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list_html.ts
index b081be7..dec65e2 100644
--- a/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list_html.ts
+++ b/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list_html.ts
@@ -42,15 +42,13 @@
display: inline-block;
}
gr-button.addReviewer {
- --padding: 1px 4px;
+ --gr-button-padding: 1px 0px;
vertical-align: top;
top: 1px;
}
gr-button {
line-height: var(--line-height-normal);
- --gr-button: {
- padding: 0px 0px;
- }
+ --gr-button-padding: 0px;
}
gr-account-chip {
line-height: var(--line-height-normal);
@@ -60,6 +58,7 @@
gr-vote-chip {
--gr-vote-chip-width: 14px;
--gr-vote-chip-height: 14px;
+ margin-right: var(--spacing-s);
}
</style>
<div class="container">
diff --git a/polygerrit-ui/app/elements/change/gr-submit-requirement-hovercard/gr-submit-requirement-hovercard.ts b/polygerrit-ui/app/elements/change/gr-submit-requirement-hovercard/gr-submit-requirement-hovercard.ts
index f636df3..3c9f54c 100644
--- a/polygerrit-ui/app/elements/change/gr-submit-requirement-hovercard/gr-submit-requirement-hovercard.ts
+++ b/polygerrit-ui/app/elements/change/gr-submit-requirement-hovercard/gr-submit-requirement-hovercard.ts
@@ -14,18 +14,14 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-import '../../../styles/gr-font-styles';
-import '../../shared/gr-hovercard/gr-hovercard-shared-style';
import '../../shared/gr-button/gr-button';
-import {PolymerElement} from '@polymer/polymer/polymer-element';
-import {customElement, property} from '@polymer/decorators';
-import {HovercardBehaviorMixin} from '../../shared/gr-hovercard/gr-hovercard-behavior';
-import {htmlTemplate} from './gr-submit-requirement-hovercard_html';
+import '../../shared/gr-label-info/gr-label-info';
+import '../../shared/gr-limited-text/gr-limited-text';
+import {customElement, property} from 'lit/decorators';
import {
AccountInfo,
SubmitRequirementExpressionInfo,
SubmitRequirementResultInfo,
- SubmitRequirementStatus,
} from '../../../api/rest-api';
import {
extractAssociatedLabels,
@@ -33,16 +29,15 @@
} from '../../../utils/label-util';
import {ParsedChangeInfo} from '../../../types/types';
import {Label} from '../gr-change-requirements/gr-change-requirements';
+import {css, html, LitElement} from 'lit';
+import {HovercardMixin} from '../../../mixins/hovercard-mixin/hovercard-mixin';
+import {fontStyles} from '../../../styles/gr-font-styles';
// This avoids JSC_DYNAMIC_EXTENDS_WITHOUT_JSDOC closure compiler error.
-const base = HovercardBehaviorMixin(PolymerElement);
+const base = HovercardMixin(LitElement);
@customElement('gr-submit-requirement-hovercard')
export class GrHovercardRun extends base {
- static get template() {
- return htmlTemplate;
- }
-
@property({type: Object})
requirement?: SubmitRequirementResultInfo;
@@ -53,21 +48,181 @@
account?: AccountInfo;
@property({type: Boolean})
- mutable?: boolean;
+ mutable = false;
@property({type: Boolean})
expanded = false;
- @property({type: Array, computed: 'computeLabels(change, requirement)'})
- _labels: Label[] = [];
+ static override get styles() {
+ return [
+ fontStyles,
+ base.styles || [],
+ css`
+ #container {
+ min-width: 356px;
+ max-width: 356px;
+ padding: var(--spacing-xl) 0 var(--spacing-m) 0;
+ }
+ section.label {
+ display: table-row;
+ }
+ .label-title {
+ min-width: 10em;
+ padding-top: var(--spacing-s);
+ }
+ .label-value {
+ padding-top: var(--spacing-s);
+ }
+ .label-title,
+ .label-value {
+ display: table-cell;
+ vertical-align: top;
+ }
+ .row {
+ display: flex;
+ }
+ .title {
+ color: var(--deemphasized-text-color);
+ margin-right: var(--spacing-m);
+ }
+ div.section {
+ margin: 0 var(--spacing-xl) var(--spacing-m) var(--spacing-xl);
+ display: flex;
+ align-items: center;
+ }
+ div.sectionIcon {
+ flex: 0 0 30px;
+ }
+ div.sectionIcon iron-icon {
+ position: relative;
+ width: 20px;
+ height: 20px;
+ }
+ .condition {
+ background-color: var(--gray-background);
+ padding: var(--spacing-m);
+ flex-grow: 1;
+ }
+ .expression {
+ color: var(--gray-foreground);
+ }
+ iron-icon.check {
+ color: var(--success-foreground);
+ }
+ iron-icon.close {
+ color: var(--warning-foreground);
+ }
+ .showConditions iron-icon {
+ color: inherit;
+ }
+ div.showConditions {
+ border-top: 1px solid var(--border-color);
+ margin-top: var(--spacing-m);
+ padding: var(--spacing-m) var(--spacing-xl) 0;
+ }
+ `,
+ ];
+ }
- computeLabels(
- change?: ParsedChangeInfo,
- requirement?: SubmitRequirementResultInfo
- ) {
- if (!requirement) return [];
- const requirementLabels = extractAssociatedLabels(requirement);
- const labels = change?.labels ?? {};
+ override render() {
+ if (!this.requirement) return;
+ const icon = iconForStatus(this.requirement.status);
+ return html` <div id="container" role="tooltip" tabindex="-1">
+ <div class="section">
+ <div class="sectionIcon">
+ <iron-icon class="${icon}" icon="gr-icons:${icon}"></iron-icon>
+ </div>
+ <div class="sectionContent">
+ <h3 class="name heading-3">
+ <span>${this.requirement.name}</span>
+ </h3>
+ </div>
+ </div>
+ <div class="section">
+ <div class="sectionIcon">
+ <iron-icon class="small" icon="gr-icons:info-outline"></iron-icon>
+ </div>
+ <div class="sectionContent">
+ <div class="row">
+ <div class="title">Status</div>
+ <div>${this.requirement.status}</div>
+ </div>
+ </div>
+ </div>
+ ${this.renderLabelSection()} ${this.renderConditionSection()}
+ </div>`;
+ }
+
+ private renderLabelSection() {
+ const labels = this.computeLabels();
+ return html` <div class="section">
+ ${labels.map(l => this.renderLabel(l))}
+ </div>`;
+ }
+
+ private renderLabel(label: Label) {
+ return html`
+ <section class="label">
+ <div class="label-title">
+ <gr-limited-text
+ class="name"
+ limit="25"
+ text="${label.labelName}"
+ ></gr-limited-text>
+ </div>
+ <div class="label-value">
+ <gr-label-info
+ .change=${this.change}
+ .account=${this.account}
+ .mutable=${this.mutable}
+ .label="${label.labelName}"
+ .labelInfo="${label.labelInfo}"
+ ></gr-label-info>
+ </div>
+ </section>
+ `;
+ }
+
+ private renderConditionSection() {
+ if (!this.expanded) {
+ return html` <div class="showConditions">
+ <gr-button
+ link=""
+ class="showConditions"
+ @click="${(_: MouseEvent) => this.handleShowConditions()}"
+ >
+ View condition
+ <iron-icon icon="gr-icons:expand-more"></iron-icon
+ ></gr-button>
+ </div>`;
+ } else {
+ return html`
+ <div class="section">
+ <div class="sectionIcon">
+ <iron-icon icon="gr-icons:description"></iron-icon>
+ </div>
+ <div class="sectionContent">${this.requirement?.description}</div>
+ </div>
+ ${this.renderCondition(
+ 'Blocking condition',
+ this.requirement?.submittability_expression_result
+ )}
+ ${this.renderCondition(
+ 'Application condition',
+ this.requirement?.applicability_expression_result
+ )}
+ ${this.renderCondition(
+ 'Override condition',
+ this.requirement?.override_expression_result
+ )}
+ `;
+ }
+ }
+
+ private computeLabels() {
+ if (!this.requirement) return [];
+ const requirementLabels = extractAssociatedLabels(this.requirement);
+ const labels = this.change?.labels ?? {};
const allLabels: Label[] = [];
@@ -84,17 +239,23 @@
return allLabels;
}
- computeIcon(status: SubmitRequirementStatus) {
- return iconForStatus(status);
- }
-
- renderCondition(expression?: SubmitRequirementExpressionInfo) {
+ private renderCondition(
+ name: string,
+ expression?: SubmitRequirementExpressionInfo
+ ) {
if (!expression) return '';
-
- return expression.expression;
+ return html`
+ <div class="section">
+ <div class="sectionIcon"></div>
+ <div class="sectionContent condition">
+ ${name}:<br />
+ <span class="expression"> ${expression.expression} </span>
+ </div>
+ </div>
+ `;
}
- _handleShowConditions() {
+ private handleShowConditions() {
this.expanded = true;
}
}
diff --git a/polygerrit-ui/app/elements/change/gr-submit-requirement-hovercard/gr-submit-requirement-hovercard_html.ts b/polygerrit-ui/app/elements/change/gr-submit-requirement-hovercard/gr-submit-requirement-hovercard_html.ts
deleted file mode 100644
index 192a812..0000000
--- a/polygerrit-ui/app/elements/change/gr-submit-requirement-hovercard/gr-submit-requirement-hovercard_html.ts
+++ /dev/null
@@ -1,189 +0,0 @@
-/**
- * @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 {html} from '@polymer/polymer/lib/utils/html-tag';
-
-export const htmlTemplate = html`
- <style include="gr-font-styles">
- /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
- </style>
- <style include="gr-hovercard-shared-style">
- #container {
- min-width: 356px;
- max-width: 356px;
- padding: var(--spacing-xl) 0 var(--spacing-m) 0;
- }
- section.label {
- display: table-row;
- }
- .label-title {
- min-width: 10em;
- padding-top: var(--spacing-s);
- }
- .label-value {
- padding-top: var(--spacing-s);
- }
- .label-title,
- .label-value {
- display: table-cell;
- vertical-align: top;
- }
- .row {
- display: flex;
- }
- .title {
- color: var(--deemphasized-text-color);
- margin-right: var(--spacing-m);
- }
- div.section {
- margin: 0 var(--spacing-xl) var(--spacing-m) var(--spacing-xl);
- display: flex;
- }
- div.sectionIcon {
- flex: 0 0 30px;
- }
- div.sectionIcon iron-icon {
- position: relative;
- top: 2px;
- width: 20px;
- height: 20px;
- }
- .condition {
- background-color: var(--gray-background);
- padding: var(--spacing-m);
- flex-grow: 1;
- }
- .expression {
- color: var(--gray-foreground);
- }
- iron-icon.check {
- color: var(--success-foreground);
- }
- iron-icon.close {
- color: var(--warning-foreground);
- }
- .showConditions iron-icon {
- color: inherit;
- }
- div.showConditions {
- border-top: 1px solid var(--border-color);
- margin-top: var(--spacing-m);
- padding: var(--spacing-m) var(--spacing-xl) 0;
- }
- </style>
- <div id="container" role="tooltip" tabindex="-1">
- <div class="section">
- <div class="sectionIcon">
- <iron-icon
- class$="[[computeIcon(requirement.status)]]"
- icon="gr-icons:[[computeIcon(requirement.status)]]"
- ></iron-icon>
- </div>
- <div class="sectionContent">
- <h3 class="name heading-3">
- <span>[[requirement.name]]</span>
- </h3>
- </div>
- </div>
- <div class="section">
- <div class="sectionIcon">
- <iron-icon class="small" icon="gr-icons:info-outline"></iron-icon>
- </div>
- <div class="sectionContent">
- <div class="row">
- <div class="title">Status</div>
- <div>[[requirement.status]]</div>
- </div>
- </div>
- </div>
- <div class="section">
- <template is="dom-repeat" items="[[_labels]]">
- <section class="label">
- <div class="label-title">
- <gr-limited-text
- class="name"
- limit="25"
- text="[[item.labelName]]"
- ></gr-limited-text>
- </div>
- <div class="label-value">
- <gr-label-info
- change="{{change}}"
- account="[[account]]"
- mutable="[[mutable]]"
- label="[[item.labelName]]"
- label-info="[[item.labelInfo]]"
- ></gr-label-info>
- </div>
- </section>
- </template>
- </div>
- <template is="dom-if" if="[[!expanded]]">
- <div class="showConditions">
- <gr-button
- link=""
- class="showConditions"
- on-click="_handleShowConditions"
- >
- View condition
- <iron-icon icon="gr-icons:expand-more"></iron-icon
- ></gr-button>
- </div>
- </template>
- <template is="dom-if" if="[[expanded]]">
- <div class="section">
- <div class="sectionIcon">
- <iron-icon icon="gr-icons:description"></iron-icon>
- </div>
- <div class="sectionContent">[[requirement.description]]</div>
- </div>
- <div class="section">
- <div class="sectionIcon"></div>
- <div class="sectionContent condition">
- Blocking condition:<br />
- <span class="expression">
- [[renderCondition(requirement.submittability_expression_result)]]
- </span>
- </div>
- </div>
- <template
- is="dom-if"
- if="[[requirement.applicability_expression_result]]"
- >
- <div class="section">
- <div class="sectionIcon"></div>
- <div class="sectionContent condition">
- Application condition:<br />
- <span class="expression">
- [[renderCondition(requirement.applicability_expression_result)]]
- </span>
- </div>
- </div>
- </template>
- <template is="dom-if" if="[[requirement.override_expression_result]]">
- <div class="section">
- <div class="sectionIcon"></div>
- <div class="sectionContent condition">
- Override condition:<br />
- <span class="expression">
- [[renderCondition(requirement.override_expression_result)]]
- </span>
- </div>
- </div>
- </template>
- </template>
- </div>
-`;
diff --git a/polygerrit-ui/app/elements/change/gr-submit-requirements/gr-submit-requirements.ts b/polygerrit-ui/app/elements/change/gr-submit-requirements/gr-submit-requirements.ts
index fa590a3..004d594 100644
--- a/polygerrit-ui/app/elements/change/gr-submit-requirements/gr-submit-requirements.ts
+++ b/polygerrit-ui/app/elements/change/gr-submit-requirements/gr-submit-requirements.ts
@@ -16,11 +16,13 @@
*/
import '../gr-submit-requirement-hovercard/gr-submit-requirement-hovercard';
import {LitElement, css, html} from 'lit';
-import {customElement, property} from 'lit/decorators';
+import {customElement, property, state} from 'lit/decorators';
import {ParsedChangeInfo} from '../../../types/types';
import {
AccountInfo,
isDetailedLabelInfo,
+ isQuickLabelInfo,
+ LabelInfo,
LabelNameToInfoMap,
SubmitRequirementResultInfo,
SubmitRequirementStatus,
@@ -28,11 +30,21 @@
import {unique} from '../../../utils/common-util';
import {
extractAssociatedLabels,
+ getAllUniqueApprovals,
+ hasNeutralStatus,
hasVotes,
iconForStatus,
} from '../../../utils/label-util';
import {fontStyles} from '../../../styles/gr-font-styles';
-import {charsOnly} from '../../../utils/string-util';
+import {charsOnly, pluralize} from '../../../utils/string-util';
+import {subscribe} from '../../lit/subscription-controller';
+import {
+ allRunsLatestPatchsetLatestAttempt$,
+ CheckRun,
+} from '../../../services/checks/checks-model';
+import {getResultsOf, hasResultsOf} from '../../../services/checks/checks-util';
+import {Category} from '../../../api/checks';
+import '../../shared/gr-vote-chip/gr-vote-chip';
@customElement('gr-submit-requirements')
export class GrSubmitRequirements extends LitElement {
@@ -45,12 +57,14 @@
@property({type: Boolean})
mutable?: boolean;
+ @state()
+ runs: CheckRun[] = [];
+
static override get styles() {
return [
fontStyles,
css`
.metadata-title {
- font-weight: var(--font-weight-bold);
color: var(--deemphasized-text-color);
padding-left: var(--metadata-horizontal-padding);
margin: 0 0 var(--spacing-s);
@@ -67,24 +81,20 @@
iron-icon.close {
color: var(--warning-foreground);
}
- .testing {
- margin-top: var(--spacing-xxl);
- padding-left: var(--metadata-horizontal-padding);
- color: var(--deemphasized-text-color);
- }
- .testing gr-button {
- min-width: 25px;
- }
- .testing * {
- visibility: hidden;
- }
- .testing:hover * {
- visibility: visible;
- }
.requirements,
- section.votes {
+ section.trigger-votes {
margin-left: var(--spacing-l);
}
+ .trigger-votes {
+ padding-top: var(--spacing-s);
+ display: flex;
+ flex-wrap: wrap;
+ gap: var(--spacing-s);
+ /* Setting max-width as defined in Submit Requirements design,
+ * to wrap overflowed items to next row.
+ */
+ max-width: 390px;
+ }
gr-limited-text.name {
font-weight: var(--font-weight-bold);
}
@@ -95,20 +105,38 @@
td {
padding: var(--spacing-s);
}
+ .votes-cell {
+ display: flex;
+ }
+ .check-error {
+ margin-right: var(--spacing-l);
+ }
+ .check-error iron-icon {
+ color: var(--error-foreground);
+ vertical-align: top;
+ }
+ gr-vote-chip {
+ margin-right: var(--spacing-s);
+ }
`,
];
}
+ constructor() {
+ super();
+ subscribe(this, allRunsLatestPatchsetLatestAttempt$, x => (this.runs = x));
+ }
+
override render() {
const submit_requirements = (this.change?.submit_requirements ?? []).filter(
req => req.status !== SubmitRequirementStatus.NOT_APPLICABLE
);
- return html` <h2
+ return html` <h3
class="metadata-title heading-3"
id="submit-requirements-caption"
>
Submit Requirements
- </h2>
+ </h3>
<table class="requirements" aria-labelledby="submit-requirements-caption">
<thead hidden>
<tr>
@@ -130,7 +158,12 @@
.text="${requirement.name}"
></gr-limited-text>
</td>
- <td>${this.renderVotes(requirement)}</td>
+ <td>
+ <div class="votes-cell">
+ ${this.renderVotes(requirement)}
+ ${this.renderChecks(requirement)}
+ </div>
+ </td>
</tr>`
)}
</tbody>
@@ -142,13 +175,11 @@
.requirement="${requirement}"
.change="${this.change}"
.account="${this.account}"
- .mutable="${this.mutable}"
+ .mutable="${this.mutable ?? false}"
></gr-submit-requirement-hovercard>
`
)}
- ${this.renderTriggerVotes(
- submit_requirements
- )}${this.renderFakeControls()}`;
+ ${this.renderTriggerVotes(submit_requirements)}`;
}
renderStatus(status: SubmitRequirementStatus) {
@@ -180,23 +211,47 @@
renderLabelVote(label: string, labels: LabelNameToInfoMap) {
const labelInfo = labels[label];
- if (!isDetailedLabelInfo(labelInfo)) return;
- const uniqueApprovals = (labelInfo.all ?? [])
+ if (isDetailedLabelInfo(labelInfo)) {
+ const uniqueApprovals = getAllUniqueApprovals(labelInfo).filter(
+ approval => !hasNeutralStatus(labelInfo, approval)
+ );
+ return uniqueApprovals.map(
+ approvalInfo =>
+ html`<gr-vote-chip
+ .vote="${approvalInfo}"
+ .label="${labelInfo}"
+ .more="${(labelInfo.all ?? []).filter(
+ other => other.value === approvalInfo.value
+ ).length > 1}"
+ ></gr-vote-chip>`
+ );
+ } else if (isQuickLabelInfo(labelInfo)) {
+ return [html`<gr-vote-chip .label="${labelInfo}"></gr-vote-chip>`];
+ } else {
+ return html``;
+ }
+ }
+
+ renderChecks(requirement: SubmitRequirementResultInfo) {
+ const requirementLabels = extractAssociatedLabels(requirement);
+ const requirementRuns = this.runs
+ .filter(run => hasResultsOf(run, Category.ERROR))
.filter(
- (approvalInfo, index, array) =>
- index === array.findIndex(other => other.value === approvalInfo.value)
- )
- .sort((a, b) => -(a.value ?? 0) + (b.value ?? 0));
- return uniqueApprovals.map(
- approvalInfo =>
- html`<gr-vote-chip
- .vote="${approvalInfo}"
- .label="${labelInfo}"
- .more="${(labelInfo.all ?? []).filter(
- other => other.value === approvalInfo.value
- ).length > 1}"
- ></gr-vote-chip>`
+ run => run.labelName && requirementLabels.includes(run.labelName)
+ );
+ const runsCount = requirementRuns.reduce(
+ (sum, run) => sum + getResultsOf(run, Category.ERROR).length,
+ 0
);
+ if (runsCount > 0) {
+ return html`<span class="check-error"
+ ><iron-icon icon="gr-icons:error"></iron-icon>${pluralize(
+ runsCount,
+ 'error'
+ )}</span
+ >`;
+ }
+ return;
}
renderTriggerVotes(submitReqs: SubmitRequirementResultInfo[]) {
@@ -210,66 +265,88 @@
.filter(label => hasVotes(labels[label]));
if (!triggerVotes.length) return;
return html`<h3 class="metadata-title heading-3">Trigger Votes</h3>
- <section class="votes">
+ <section class="trigger-votes">
${triggerVotes.map(
- label => html`${label}:
- <gr-label-info
- .change="${this.change}"
- .account="${this.account}"
- .mutable="${this.mutable}"
- label="${label}"
+ label =>
+ html`<gr-trigger-vote
+ .label="${label}"
.labelInfo="${labels[label]}"
- ></gr-label-info>`
+ ></gr-trigger-vote>`
)}
</section>`;
}
+}
- renderFakeControls() {
+@customElement('gr-trigger-vote')
+export class GrTriggerVote extends LitElement {
+ @property()
+ label?: string;
+
+ @property({type: Object})
+ labelInfo?: LabelInfo;
+
+ static override get styles() {
+ return css`
+ :host {
+ display: block;
+ }
+ .container {
+ box-sizing: border-box;
+ border: 1px solid var(--border-color);
+ border-radius: calc(var(--border-radius) + 2px);
+ background-color: var(--background-color-primary);
+ display: flex;
+ padding: 0;
+ padding-left: var(--spacing-s);
+ padding-right: var(--spacing-xxs);
+ align-items: center;
+ }
+ .label {
+ padding-right: var(--spacing-s);
+ font-weight: var(--font-weight-bold);
+ }
+ gr-vote-chip {
+ --gr-vote-chip-width: 14px;
+ --gr-vote-chip-height: 14px;
+ margin-right: 0px;
+ }
+ `;
+ }
+
+ override render() {
+ if (!this.labelInfo) return;
return html`
- <div class="testing">
- <div>Toggle fake data:</div>
- <gr-button link @click="${() => this.renderFakeSubmitRequirements()}"
- >G</gr-button
- >
+ <div class="container">
+ <span class="label">${this.label}</span>
+ ${this.renderVotes()}
</div>
`;
}
- renderFakeSubmitRequirements() {
- if (!this.change) return;
- this.change = {
- ...this.change,
- submit_requirements: [
- {
- name: 'Code-Review',
- status: SubmitRequirementStatus.SATISFIED,
- description:
- "At least one maximum vote for label 'Code-Review' is required",
- submittability_expression_result: {
- expression: 'label:Code-Review=MAX -label:Code-Review=MIN',
- fulfilled: true,
- passing_atoms: [],
- failing_atoms: [],
- },
- },
- {
- name: 'Verified',
- status: SubmitRequirementStatus.UNSATISFIED,
- description: 'CI build and tests results are verified',
- submittability_expression_result: {
- expression: 'label:Verified=MAX -label:Verified=MIN',
- fulfilled: false,
- passing_atoms: [],
- failing_atoms: [],
- },
- },
- ],
- };
+ private renderVotes() {
+ const {labelInfo} = this;
+ if (!labelInfo) return;
+ if (isDetailedLabelInfo(labelInfo)) {
+ const approvals = getAllUniqueApprovals(labelInfo).filter(
+ approval => !hasNeutralStatus(labelInfo, approval)
+ );
+ return approvals.map(
+ approvalInfo => html`<gr-vote-chip
+ .vote="${approvalInfo}"
+ .label="${labelInfo}"
+ ></gr-vote-chip>`
+ );
+ } else if (isQuickLabelInfo(labelInfo)) {
+ return [html`<gr-vote-chip .label="${this.labelInfo}"></gr-vote-chip>`];
+ } else {
+ return html``;
+ }
}
}
declare global {
interface HTMLElementTagNameMap {
'gr-submit-requirements': GrSubmitRequirements;
+ 'gr-trigger-vote': GrTriggerVote;
}
}
diff --git a/polygerrit-ui/app/elements/change/gr-thread-list/gr-thread-list_html.ts b/polygerrit-ui/app/elements/change/gr-thread-list/gr-thread-list_html.ts
index 6ed5a2c..3eb28c9 100644
--- a/polygerrit-ui/app/elements/change/gr-thread-list/gr-thread-list_html.ts
+++ b/polygerrit-ui/app/elements/change/gr-thread-list/gr-thread-list_html.ts
@@ -51,11 +51,8 @@
margin-right: var(--spacing-s);
}
gr-dropdown-list {
- --trigger-style: {
- color: var(--primary-text-color);
- text-transform: none;
- font-family: var(--font-family);
- }
+ --trigger-style-text-color: var(--primary-text-color);
+ --trigger-style-font-family: var(--font-family);
}
.filter-text, .sort-text, .author-text {
margin-right: var(--spacing-s);
diff --git a/polygerrit-ui/app/elements/checks/gr-checks-action.ts b/polygerrit-ui/app/elements/checks/gr-checks-action.ts
index 87cab46..859fd33 100644
--- a/polygerrit-ui/app/elements/checks/gr-checks-action.ts
+++ b/polygerrit-ui/app/elements/checks/gr-checks-action.ts
@@ -22,11 +22,11 @@
@customElement('gr-checks-action')
export class GrChecksAction extends LitElement {
- @property()
+ @property({type: Object})
action!: Action;
- @property()
- eventTarget?: EventTarget;
+ @property({type: Object})
+ eventTarget: HTMLElement | null = null;
private checksService = appContext.checksService;
@@ -43,7 +43,7 @@
white-space: nowrap;
}
gr-button {
- --padding: var(--spacing-s) var(--spacing-m);
+ --gr-button-padding: var(--spacing-s) var(--spacing-m);
}
paper-tooltip {
text-transform: none;
@@ -72,7 +72,7 @@
private renderTooltip() {
if (!this.action.tooltip) return;
return html`
- <paper-tooltip offset="5" fit-to-visible-bounds="true">
+ <paper-tooltip offset="5" fit-to-visible-bounds>
${this.action.tooltip}
</paper-tooltip>
`;
diff --git a/polygerrit-ui/app/elements/checks/gr-checks-attempt.ts b/polygerrit-ui/app/elements/checks/gr-checks-attempt.ts
index b4d87ae..69152b2 100644
--- a/polygerrit-ui/app/elements/checks/gr-checks-attempt.ts
+++ b/polygerrit-ui/app/elements/checks/gr-checks-attempt.ts
@@ -21,7 +21,7 @@
@customElement('gr-checks-attempt')
class GrChecksAttempt extends LitElement {
- @property()
+ @property({attribute: false})
run?: CheckRun;
static override get styles() {
diff --git a/polygerrit-ui/app/elements/checks/gr-checks-results.ts b/polygerrit-ui/app/elements/checks/gr-checks-results.ts
index 90f7215..9c27cdb 100644
--- a/polygerrit-ui/app/elements/checks/gr-checks-results.ts
+++ b/polygerrit-ui/app/elements/checks/gr-checks-results.ts
@@ -20,7 +20,9 @@
import {LitElement, css, html, PropertyValues, TemplateResult} from 'lit';
import {customElement, property, query, state} from 'lit/decorators';
import './gr-checks-action';
+import './gr-hovercard-run';
import '@polymer/paper-tooltip/paper-tooltip';
+import '@polymer/iron-icon/iron-icon';
import {
Action,
Category,
@@ -46,7 +48,6 @@
iconForLink,
isCategory,
otherPrimaryLinks,
- primaryRunAction,
secondaryLinks,
tooltipForLink,
} from '../../services/checks/checks-util';
@@ -80,19 +81,19 @@
@query('td.nameCol div.name')
nameEl?: HTMLElement;
- @property()
+ @property({attribute: false})
result?: RunResult;
- @property()
+ @state()
isExpanded = false;
@property({type: Boolean, reflect: true})
isExpandable = false;
- @property()
+ @state()
shouldRender = false;
- @property()
+ @state()
labels?: LabelNameToInfoMap;
private checksService = appContext.checksService;
@@ -279,11 +280,10 @@
];
}
- override update(changedProperties: PropertyValues) {
+ override updated(changedProperties: PropertyValues) {
if (changedProperties.has('result')) {
this.isExpandable = !!this.result?.summary && !!this.result?.message;
}
- super.update(changedProperties);
}
override focus() {
@@ -293,7 +293,13 @@
override firstUpdated() {
const loading = this.shadowRoot?.querySelector('.container');
assertIsDefined(loading, '"Loading" element');
- whenVisible(loading, () => this.setAttribute('shouldRender', 'true'), 200);
+ whenVisible(
+ loading,
+ () => {
+ this.shouldRender = true;
+ },
+ 200
+ );
}
override render() {
@@ -324,7 +330,6 @@
${this.result.checkName}
</div>
<div class="space"></div>
- ${this.renderPrimaryRunAction()}
</div>
</td>
<td class="summaryCol">
@@ -347,7 +352,7 @@
role="switch"
tabindex="0"
?hidden="${!this.isExpandable}"
- ?aria-checked="${this.isExpanded}"
+ aria-checked="${this.isExpanded ? 'true' : 'false'}"
aria-label="${this.isExpanded
? 'Collapse result row'
: 'Expand result row'}"
@@ -367,13 +372,6 @@
`;
}
- private renderPrimaryRunAction() {
- if (!this.result) return;
- const action = primaryRunAction(this.result);
- if (!action) return;
- return html`<gr-checks-action .action="${action}"></gr-checks-action>`;
- }
-
private renderExpanded() {
if (!this.isExpanded) return;
return html`<gr-result-expanded
@@ -428,7 +426,7 @@
return html`
<div class="label ${status}">
<span>${label} ${valueStr}</span>
- <paper-tooltip offset="5" fit-to-visible-bounds="true">
+ <paper-tooltip offset="5" ?fitToVisibleBounds="${true}">
The check result has (probably) influenced this label vote.
</paper-tooltip>
</div>
@@ -525,7 +523,7 @@
renderTag(tag: Tag) {
return html`<div class="tag ${tag.color}">
<span>${tag.name}</span>
- <paper-tooltip offset="5" fit-to-visible-bounds="true">
+ <paper-tooltip offset="5" ?fitToVisibleBounds="${true}">
${tag.tooltip ?? 'A category tag for this check result'}
</paper-tooltip>
</div>`;
@@ -534,10 +532,10 @@
@customElement('gr-result-expanded')
class GrResultExpanded extends LitElement {
- @property()
+ @property({attribute: false})
result?: RunResult;
- @property()
+ @state()
repoConfig?: ConfigInfo;
private changeService = appContext.changeService;
@@ -681,36 +679,36 @@
filterRegExp = new RegExp('');
/** All runs. Shown should only the selected/filtered ones. */
- @property()
+ @property({attribute: false})
runs: CheckRun[] = [];
/**
* Check names of runs that are selected in the runs panel. When this array
* is empty, then no run is selected and all runs should be shown.
*/
- @property()
+ @property({attribute: false})
selectedRuns: string[] = [];
- @property()
+ @state()
actions: Action[] = [];
- @property()
+ @state()
links: Link[] = [];
- @property()
+ @property({attribute: false})
tabState?: ChecksTabState;
- @property()
+ @state()
someProvidersAreLoading = false;
- @property()
+ @state()
checksPatchsetNumber: PatchSetNumber | undefined = undefined;
- @property()
+ @state()
latestPatchsetNumber: PatchSetNumber | undefined = undefined;
/** Maps checkName to selected attempt number. `undefined` means `latest`. */
- @property()
+ @property({attribute: false})
selectedAttempts: Map<string, number | undefined> = new Map<
string,
number | undefined
@@ -817,6 +815,7 @@
}
.headerTopRow .right .goToLatest gr-button {
margin-right: var(--spacing-m);
+ --gr-button-padding: var(--spacing-s) var(--spacing-m);
}
.headerBottomRow iron-icon {
color: var(--link-color);
@@ -976,22 +975,12 @@
}
override render() {
- // To pass CSS mixins for @apply to Polymer components, they need to appear
- // in <style> inside the template.
- const style = html`<style>
- .headerTopRow .right .goToLatest gr-button {
- --gr-button: {
- padding: var(--spacing-s) var(--spacing-m);
- text-transform: none;
- }
- }
- </style>`;
- const headerClasses = classMap({
+ const headerClasses = {
header: true,
notLatest: !!this.checksPatchsetNumber,
- });
- return html`${style}
- <div class="${headerClasses}">
+ };
+ return html`
+ <div class="${classMap(headerClasses)}">
<div class="headerTopRow">
<div class="left">
<h2 class="heading-2">Results</h2>
@@ -1007,7 +996,9 @@
>
</div>
<gr-dropdown-list
- value="${this.checksPatchsetNumber ?? this.latestPatchsetNumber}"
+ value="${this.checksPatchsetNumber ??
+ this.latestPatchsetNumber ??
+ 0}"
.items="${this.createPatchsetDropdownItems()}"
@value-change="${this.onPatchsetSelected}"
></gr-dropdown-list>
@@ -1023,7 +1014,8 @@
${this.renderSection(Category.WARNING)}
${this.renderSection(Category.INFO)}
${this.renderSection(Category.SUCCESS)}
- </div>`;
+ </div>
+ `;
}
private renderLinksAndActions() {
@@ -1295,9 +1287,9 @@
${repeat(
filtered,
result => result.internalResultId,
- result => html`
+ (result?: RunResult) => html`
<gr-result-row
- class="${charsOnly(result.checkName)}"
+ class="${charsOnly(result!.checkName)}"
.result="${result}"
></gr-result-row>
`
diff --git a/polygerrit-ui/app/elements/checks/gr-checks-runs.ts b/polygerrit-ui/app/elements/checks/gr-checks-runs.ts
index 28354d2..a643c18 100644
--- a/polygerrit-ui/app/elements/checks/gr-checks-runs.ts
+++ b/polygerrit-ui/app/elements/checks/gr-checks-runs.ts
@@ -14,6 +14,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
+import '@polymer/iron-icon/iron-icon';
import {classMap} from 'lit/directives/class-map';
import './gr-hovercard-run';
import {css, html, LitElement, nothing, PropertyValues} from 'lit';
@@ -182,28 +183,24 @@
@query('.chip')
chipElement?: HTMLElement;
- @property()
+ @property({attribute: false})
run!: CheckRun;
- @property()
+ @property({attribute: false})
selected = false;
- @property()
+ @property({attribute: false})
selectedAttempt?: number;
- @property()
+ @property({attribute: false})
deselected = false;
- @property()
+ @state()
shouldRender = false;
override firstUpdated() {
assertIsDefined(this.chipElement, 'chip element');
- whenVisible(
- this.chipElement,
- () => this.setAttribute('shouldRender', 'true'),
- 200
- );
+ whenVisible(this.chipElement, () => (this.shouldRender = true), 200);
}
protected override updated(changedProperties: PropertyValues) {
@@ -303,9 +300,6 @@
renderStatusLink() {
const link = this.run.statusLink;
if (!link) return;
- // For COMPLETED we think that the status link are too much clutter.
- // That could be re-considered.
- if (this.run.status !== RunStatus.RUNNING) return;
return html`
<a href="${link}" target="_blank" @click="${this.onLinkClick}"
><iron-icon
@@ -368,29 +362,29 @@
@state()
filterRegExp = new RegExp('');
- @property()
+ @property({attribute: false})
runs: CheckRun[] = [];
@property({type: Boolean, reflect: true})
collapsed = false;
- @property()
+ @property({attribute: false})
selectedRuns: string[] = [];
/** Maps checkName to selected attempt number. `undefined` means `latest`. */
- @property()
+ @property({attribute: false})
selectedAttempts: Map<string, number | undefined> = new Map<
string,
number | undefined
>();
- @property()
+ @property({attribute: false})
tabState?: ChecksTabState;
- @property()
+ @state()
errorMessages: ErrorMessages = {};
- @property()
+ @state()
loginCallback?: () => void;
private isSectionExpanded = new Map<RunStatus, boolean>();
@@ -429,11 +423,11 @@
flex-grow: 1;
}
.title gr-button {
- --padding: var(--spacing-s) var(--spacing-m);
+ --gr-button-padding: var(--spacing-s) var(--spacing-m);
white-space: nowrap;
}
.title gr-button.expandButton {
- --padding: var(--spacing-xs) var(--spacing-s);
+ --gr-button-padding: var(--spacing-xs) var(--spacing-s);
}
:host(:not([collapsed])) .expandButton {
margin-right: calc(0px - var(--spacing-m));
@@ -475,6 +469,11 @@
.testing:hover * {
visibility: visible;
}
+ .zero {
+ padding: var(--spacing-m) 0;
+ color: var(--primary-text-color);
+ margin-top: var(--spacing-m);
+ }
.login,
.error {
padding: var(--spacing-m);
@@ -534,7 +533,7 @@
<div class="flex-space"></div>
${this.renderTitleButtons()} ${this.renderCollapseButton()}
</h2>
- ${this.renderErrors()} ${this.renderSignIn()}
+ ${this.renderErrors()} ${this.renderSignIn()} ${this.renderZeroState()}
<input
id="filterInput"
type="text"
@@ -548,6 +547,11 @@
`;
}
+ private renderZeroState() {
+ if (this.runs.length > 0) return;
+ return html`<div class="zero">No Check Run to show</div>`;
+ }
+
private renderErrors() {
return Object.entries(this.errorMessages).map(
([plugin, message]) =>
@@ -604,42 +608,48 @@
@click="${() => fireRunSelectionReset(this)}"
>Unselect All</gr-button
>
- <gr-button
- class="font-normal"
- link
+ <gr-tooltip-content
title="${runButtonDisabled
? 'Disabled. Unselect checks without a "Run" action to enable the button.'
: ''}"
- has-tooltip="${runButtonDisabled}"
- ?disabled="${runButtonDisabled}"
- @click="${() => {
- actions.forEach(action => this.checksService.triggerAction(action));
- }}"
- >Run Selected</gr-button
+ ?has-tooltip=${runButtonDisabled}
>
+ <gr-button
+ class="font-normal"
+ link
+ ?disabled=${runButtonDisabled}
+ @click="${() => {
+ actions.forEach(action => this.checksService.triggerAction(action));
+ }}"
+ >Run Selected</gr-button
+ >
+ </gr-tooltip-content>
`;
}
private renderCollapseButton() {
return html`
- <gr-button
- link
- class="expandButton"
- role="switch"
- ?aria-checked="${this.collapsed}"
- aria-label="${this.collapsed
- ? 'Expand runs panel'
- : 'Collapse runs panel'}"
- has-tooltip="true"
+ <gr-tooltip-content
+ has-tooltip
title="${this.collapsed ? 'Expand runs panel' : 'Collapse runs panel'}"
- @click="${() => (this.collapsed = !this.collapsed)}"
- ><iron-icon
- class="expandIcon"
- icon="${this.collapsed
- ? 'gr-icons:chevron-right'
- : 'gr-icons:chevron-left'}"
- ></iron-icon>
- </gr-button>
+ >
+ <gr-button
+ link
+ class="expandButton"
+ role="switch"
+ aria-checked="${this.collapsed ? 'true' : 'false'}"
+ aria-label="${this.collapsed
+ ? 'Expand runs panel'
+ : 'Collapse runs panel'}"
+ @click="${() => (this.collapsed = !this.collapsed)}"
+ ><iron-icon
+ class="expandIcon"
+ icon="${this.collapsed
+ ? 'gr-icons:chevron-right'
+ : 'gr-icons:chevron-left'}"
+ ></iron-icon>
+ </gr-button>
+ </gr-tooltip-content>
`;
}
diff --git a/polygerrit-ui/app/elements/checks/gr-checks-tab.ts b/polygerrit-ui/app/elements/checks/gr-checks-tab.ts
index 688667a..ed6117a 100644
--- a/polygerrit-ui/app/elements/checks/gr-checks-tab.ts
+++ b/polygerrit-ui/app/elements/checks/gr-checks-tab.ts
@@ -40,22 +40,22 @@
*/
@customElement('gr-checks-tab')
export class GrChecksTab extends LitElement {
- @property()
+ @state()
runs: CheckRun[] = [];
- @property()
+ @state()
results: CheckResult[] = [];
- @property()
+ @property({type: Object})
tabState?: ChecksTabState;
- @property()
+ @state()
checksPatchsetNumber: PatchSetNumber | undefined = undefined;
- @property()
+ @state()
latestPatchsetNumber: PatchSetNumber | undefined = undefined;
- @property()
+ @state()
changeNum: NumericChangeId | undefined = undefined;
@state()
diff --git a/polygerrit-ui/app/elements/checks/gr-hovercard-run.ts b/polygerrit-ui/app/elements/checks/gr-hovercard-run.ts
index d26856c..95b7157 100644
--- a/polygerrit-ui/app/elements/checks/gr-hovercard-run.ts
+++ b/polygerrit-ui/app/elements/checks/gr-hovercard-run.ts
@@ -14,12 +14,8 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-import './gr-checks-styles';
-import '../../styles/gr-font-styles';
-import {HovercardBehaviorMixin} from '../shared/gr-hovercard/gr-hovercard-behavior';
-import {PolymerElement} from '@polymer/polymer/polymer-element';
-import {htmlTemplate} from './gr-hovercard-run_html';
-import {customElement, property} from '@polymer/decorators';
+import {fontStyles} from '../../styles/gr-font-styles';
+import {customElement, property} from 'lit/decorators';
import './gr-checks-action';
import {CheckRun} from '../../services/checks/checks-model';
import {
@@ -31,102 +27,346 @@
import {durationString, fromNow} from '../../utils/date-util';
import {RunStatus} from '../../api/checks';
import {ordinal} from '../../utils/string-util';
+import {HovercardMixin} from '../../mixins/hovercard-mixin/hovercard-mixin';
+import {css, html, LitElement} from 'lit';
+import {checksStyles} from './gr-checks-styles';
// This avoids JSC_DYNAMIC_EXTENDS_WITHOUT_JSDOC closure compiler error.
-const base = HovercardBehaviorMixin(PolymerElement);
+const base = HovercardMixin(LitElement);
@customElement('gr-hovercard-run')
export class GrHovercardRun extends base {
- static get template() {
- return htmlTemplate;
- }
-
@property({type: Object})
run?: CheckRun;
- computeIcon(run?: CheckRun) {
- if (!run) return '';
- const category = worstCategory(run);
+ static override get styles() {
+ return [
+ fontStyles,
+ checksStyles,
+ base.styles || [],
+ css`
+ #container {
+ min-width: 356px;
+ max-width: 356px;
+ padding: var(--spacing-xl) 0 var(--spacing-m) 0;
+ }
+ .row {
+ display: flex;
+ margin-top: var(--spacing-s);
+ }
+ .attempts.row {
+ flex-wrap: wrap;
+ }
+ .chipRow {
+ display: flex;
+ margin-top: var(--spacing-s);
+ }
+ .chip {
+ background: var(--gray-background);
+ color: var(--gray-foreground);
+ border-radius: 20px;
+ padding: var(--spacing-xs) var(--spacing-m) var(--spacing-xs)
+ var(--spacing-s);
+ }
+ .title {
+ color: var(--deemphasized-text-color);
+ margin-right: var(--spacing-m);
+ }
+ div.section {
+ margin: 0 var(--spacing-xl) var(--spacing-m) var(--spacing-xl);
+ display: flex;
+ }
+ div.sectionIcon {
+ flex: 0 0 30px;
+ }
+ div.chip iron-icon {
+ width: 16px;
+ height: 16px;
+ /* Positioning of a 16px icon in the middle of a 20px line. */
+ position: relative;
+ top: 2px;
+ }
+ div.sectionIcon iron-icon {
+ position: relative;
+ top: 2px;
+ width: 20px;
+ height: 20px;
+ }
+ div.sectionIcon iron-icon.small {
+ position: relative;
+ top: 6px;
+ width: 16px;
+ height: 16px;
+ }
+ div.sectionContent iron-icon.link {
+ color: var(--link-color);
+ }
+ div.sectionContent .attemptIcon iron-icon,
+ div.sectionContent iron-icon.small {
+ width: 16px;
+ height: 16px;
+ margin-right: var(--spacing-s);
+ /* Positioning of a 16px icon in the middle of a 20px line. */
+ position: relative;
+ top: 2px;
+ }
+ div.sectionContent .attemptIcon iron-icon {
+ margin-right: 0;
+ }
+ .attemptIcon,
+ .attemptNumber {
+ margin-right: var(--spacing-s);
+ color: var(--deemphasized-text-color);
+ text-align: center;
+ width: 24px;
+ font-size: var(--font-size-small);
+ }
+ div.action {
+ border-top: 1px solid var(--border-color);
+ margin-top: var(--spacing-m);
+ padding: var(--spacing-m) var(--spacing-xl) 0;
+ }
+ `,
+ ];
+ }
+
+ override render() {
+ if (!this.run) return '';
+ const icon = this.computeIcon();
+ return html`
+ <div id="container" role="tooltip" tabindex="-1">
+ <div class="section">
+ <div
+ ?hidden="${!this.run || this.run.status === RunStatus.RUNNABLE}"
+ class="chipRow"
+ >
+ <div class="chip">
+ <iron-icon icon="gr-icons:${this.computeChipIcon()}"></iron-icon>
+ <span>${this.run.status}</span>
+ </div>
+ </div>
+ </div>
+ <div class="section">
+ <div class="sectionIcon" ?hidden="${icon.length === 0}">
+ <iron-icon class="${icon}" icon="gr-icons:${icon}"></iron-icon>
+ </div>
+ <div class="sectionContent">
+ <h3 class="name heading-3">
+ <span>${this.run.checkName}</span>
+ </h3>
+ </div>
+ </div>
+ ${this.renderStatusSection()} ${this.renderAttemptSection()}
+ ${this.renderTimestampSection()} ${this.renderDescriptionSection()}
+ ${this.renderActions()}
+ </div>
+ `;
+ }
+
+ private renderStatusSection() {
+ if (!this.run || (!this.run.statusLink && !this.run.statusDescription))
+ return;
+
+ return html`
+ <div class="section">
+ <div class="sectionIcon">
+ <iron-icon class="small" icon="gr-icons:info-outline"></iron-icon>
+ </div>
+ <div class="sectionContent">
+ ${this.run.statusLink
+ ? html` <div class="row">
+ <div class="title">Status</div>
+ <div>
+ <a href="${this.run.statusLink}" target="_blank"
+ ><iron-icon
+ aria-label="external link to check status"
+ class="small link"
+ icon="gr-icons:launch"
+ ></iron-icon
+ >${this.computeHostName(this.run.statusLink)}
+ </a>
+ </div>
+ </div>`
+ : ''}
+ ${this.run.statusDescription
+ ? html` <div class="row">
+ <div class="title">Message</div>
+ <div>${this.run.statusDescription}</div>
+ </div>`
+ : ''}
+ </div>
+ </div>
+ `;
+ }
+
+ private renderAttemptSection() {
+ if (this.hideAttempts()) return;
+ const attempts = this.computeAttempts();
+ return html`
+ <div class="section">
+ <div class="sectionIcon">
+ <iron-icon class="small" icon="gr-icons:arrow-forward"></iron-icon>
+ </div>
+ <div class="sectionContent">
+ <div class="attempts row">
+ <div class="title">Attempt</div>
+ ${attempts.map(a => this.renderAttempt(a))}
+ </div>
+ </div>
+ </div>
+ `;
+ }
+
+ private renderAttempt(attempt: AttemptDetail) {
+ return html`
+ <div>
+ <div class="attemptIcon">
+ <iron-icon
+ class="${attempt.icon}"
+ icon="gr-icons:${attempt.icon}"
+ ></iron-icon>
+ </div>
+ <div class="attemptNumber">${ordinal(attempt.attempt)}</div>
+ </div>
+ `;
+ }
+
+ private renderTimestampSection() {
+ if (
+ !this.run ||
+ (!this.run.startedTimestamp &&
+ !this.run.scheduledTimestamp &&
+ !this.run.finishedTimestamp)
+ )
+ return;
+
+ return html`
+ <div class="section">
+ <div class="sectionIcon">
+ <iron-icon class="small" icon="gr-icons:schedule"></iron-icon>
+ </div>
+ <div class="sectionContent">
+ <div ?hidden="${this.hideScheduled()}" class="row">
+ <div class="title">Scheduled</div>
+ <div>${this.computeDuration(this.run.scheduledTimestamp)}</div>
+ </div>
+ <div ?hidden="${!this.run.startedTimestamp}" class="row">
+ <div class="title">Started</div>
+ <div>${this.computeDuration(this.run.startedTimestamp)}</div>
+ </div>
+ <div ?hidden="${!this.run.finishedTimestamp}" class="row">
+ <div class="title">Ended</div>
+ <div>${this.computeDuration(this.run.finishedTimestamp)}</div>
+ </div>
+ <div ?hidden="${this.hideCompletion()}" class="row">
+ <div class="title">Completion</div>
+ <div>${this.computeCompletionDuration()}</div>
+ </div>
+ </div>
+ </div>
+ `;
+ }
+
+ private renderDescriptionSection() {
+ if (!this.run || (!this.run.checkLink && !this.run.checkDescription))
+ return;
+ return html`
+ <div class="section">
+ <div class="sectionIcon">
+ <iron-icon class="small" icon="gr-icons:link"></iron-icon>
+ </div>
+ <div class="sectionContent">
+ ${this.run.checkDescription
+ ? html` <div class="row">
+ <div class="title">Description</div>
+ <div>${this.run.checkDescription}</div>
+ </div>`
+ : ''}
+ ${this.run.checkLink
+ ? html` <div class="row">
+ <div class="title">Documentation</div>
+ <div>
+ <a href="${this.run.checkLink}" target="_blank"
+ ><iron-icon
+ aria-label="external link to check documentation"
+ class="small link"
+ icon="gr-icons:launch"
+ ></iron-icon
+ >${this.computeHostName(this.run.checkLink)}
+ </a>
+ </div>
+ </div>`
+ : ''}
+ </div>
+ </div>
+ `;
+ }
+
+ private renderActions() {
+ const actions = runActions(this.run);
+ return actions.map(
+ action =>
+ html`
+ <div class="action">
+ <gr-checks-action
+ .eventTarget="${this._target}"
+ .action="${action}"
+ ></gr-checks-action>
+ </div>
+ `
+ );
+ }
+
+ computeIcon() {
+ if (!this.run) return '';
+ const category = worstCategory(this.run);
if (category) return iconFor(category);
- return run.status === RunStatus.COMPLETED
+ return this.run.status === RunStatus.COMPLETED
? iconFor(RunStatus.COMPLETED)
: '';
}
- computeActions(run?: CheckRun) {
- return runActions(run);
- }
-
- computeAttempt(attempt?: number) {
- return ordinal(attempt);
- }
-
- computeAttempts(run?: CheckRun): AttemptDetail[] {
- const details = run?.attemptDetails ?? [];
+ computeAttempts(): AttemptDetail[] {
+ const details = this.run?.attemptDetails ?? [];
const more =
details.length > 7 ? [{icon: 'more-horiz', attempt: undefined}] : [];
return [...more, ...details.slice(-7)];
}
- computeChipIcon(run?: CheckRun) {
- if (run?.status === RunStatus.COMPLETED) return 'check';
- if (run?.status === RunStatus.RUNNING) return 'timelapse';
+ private computeChipIcon() {
+ if (this.run?.status === RunStatus.COMPLETED) return 'check';
+ if (this.run?.status === RunStatus.RUNNING) return 'timelapse';
return '';
}
- computeCompletionDuration(run?: CheckRun) {
- if (!run?.finishedTimestamp || !run?.startedTimestamp) return '';
- return durationString(run.startedTimestamp, run.finishedTimestamp, true);
- }
-
- computeDuration(date?: Date) {
- return date ? fromNow(date) : '';
- }
-
- computeHostName(link?: string) {
- return link ? new URL(link).hostname : '';
- }
-
- hideChip(run?: CheckRun) {
- return !run || run.status === RunStatus.RUNNABLE;
- }
-
- hideHeaderSectionIcon(run?: CheckRun) {
- return this.computeIcon(run).length === 0;
- }
-
- hideStatusSection(run?: CheckRun) {
- if (!run) return true;
- return !run.statusLink && !run.statusDescription;
- }
-
- hideTimestampSection(run?: CheckRun) {
- if (!run) return true;
- return (
- !run.startedTimestamp && !run.scheduledTimestamp && !run.finishedTimestamp
+ private computeCompletionDuration() {
+ if (!this.run?.finishedTimestamp || !this.run?.startedTimestamp) return '';
+ return durationString(
+ this.run.startedTimestamp,
+ this.run.finishedTimestamp,
+ true
);
}
- hideAttempts(run?: CheckRun) {
- const attemptCount = run?.attemptDetails?.length;
+ private computeDuration(date?: Date) {
+ return date ? fromNow(date) : '';
+ }
+
+ private computeHostName(link?: string) {
+ return link ? new URL(link).hostname : '';
+ }
+
+ private hideAttempts() {
+ const attemptCount = this.run?.attemptDetails?.length;
return attemptCount === undefined || attemptCount < 2;
}
- hideScheduled(run?: CheckRun) {
- return !run?.scheduledTimestamp || !!run?.startedTimestamp;
+ private hideScheduled() {
+ return !this.run?.scheduledTimestamp || !!this.run?.startedTimestamp;
}
- hideCompletion(run?: CheckRun) {
- return !run?.startedTimestamp || !run?.finishedTimestamp;
- }
-
- hideDescriptionSection(run?: CheckRun) {
- if (!run) return true;
- return !run.checkLink && !run.checkDescription;
- }
-
- _convertUndefined(value?: string) {
- return value ?? '';
+ private hideCompletion() {
+ return !this.run?.startedTimestamp || !this.run?.finishedTimestamp;
}
}
diff --git a/polygerrit-ui/app/elements/checks/gr-hovercard-run_html.ts b/polygerrit-ui/app/elements/checks/gr-hovercard-run_html.ts
deleted file mode 100644
index 49a1416..0000000
--- a/polygerrit-ui/app/elements/checks/gr-hovercard-run_html.ts
+++ /dev/null
@@ -1,232 +0,0 @@
-/**
- * @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 {html} from '@polymer/polymer/lib/utils/html-tag';
-
-export const htmlTemplate = html`
- <style include="gr-font-styles">
- /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
- </style>
- <style include="gr-checks-styles">
- /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
- </style>
- <style include="gr-hovercard-shared-style">
- #container {
- min-width: 356px;
- max-width: 356px;
- padding: var(--spacing-xl) 0 var(--spacing-m) 0;
- }
- .row {
- display: flex;
- margin-top: var(--spacing-s);
- }
- .attempts.row {
- flex-wrap: wrap;
- }
- .chipRow {
- display: flex;
- margin-top: var(--spacing-s);
- }
- .chip {
- background: var(--gray-background);
- color: var(--gray-foreground);
- border-radius: 20px;
- padding: var(--spacing-xs) var(--spacing-m) var(--spacing-xs)
- var(--spacing-s);
- }
- .title {
- color: var(--deemphasized-text-color);
- margin-right: var(--spacing-m);
- }
- div.section {
- margin: 0 var(--spacing-xl) var(--spacing-m) var(--spacing-xl);
- display: flex;
- }
- div.sectionIcon {
- flex: 0 0 30px;
- }
- div.chip iron-icon {
- width: 16px;
- height: 16px;
- /* Positioning of a 16px icon in the middle of a 20px line. */
- position: relative;
- top: 2px;
- }
- div.sectionIcon iron-icon {
- position: relative;
- top: 2px;
- width: 20px;
- height: 20px;
- }
- div.sectionIcon iron-icon.small {
- position: relative;
- top: 6px;
- width: 16px;
- height: 16px;
- }
- div.sectionContent iron-icon.link {
- color: var(--link-color);
- }
- div.sectionContent .attemptIcon iron-icon,
- div.sectionContent iron-icon.small {
- width: 16px;
- height: 16px;
- margin-right: var(--spacing-s);
- /* Positioning of a 16px icon in the middle of a 20px line. */
- position: relative;
- top: 2px;
- }
- div.sectionContent .attemptIcon iron-icon {
- margin-right: 0;
- }
- .attemptIcon,
- .attemptNumber {
- margin-right: var(--spacing-s);
- color: var(--deemphasized-text-color);
- text-align: center;
- width: 24px;
- font-size: var(--font-size-small);
- }
- div.action {
- border-top: 1px solid var(--border-color);
- margin-top: var(--spacing-m);
- padding: var(--spacing-m) var(--spacing-xl) 0;
- }
- </style>
- <div id="container" role="tooltip" tabindex="-1">
- <div class="section">
- <div hidden$="[[hideChip(run)]]" class="chipRow">
- <div class="chip">
- <iron-icon icon="gr-icons:[[computeChipIcon(run)]]"></iron-icon>
- <span>[[run.status]]</span>
- </div>
- </div>
- </div>
- <div class="section">
- <div class="sectionIcon" hidden$="[[hideHeaderSectionIcon(run)]]">
- <iron-icon
- class$="[[computeIcon(run)]]"
- icon="gr-icons:[[computeIcon(run)]]"
- ></iron-icon>
- </div>
- <div class="sectionContent">
- <h3 class="name heading-3">
- <span>[[run.checkName]]</span>
- </h3>
- </div>
- </div>
- <div class="section" hidden$="[[hideStatusSection(run)]]">
- <div class="sectionIcon">
- <iron-icon class="small" icon="gr-icons:info-outline"></iron-icon>
- </div>
- <div class="sectionContent">
- <div hidden$="[[!run.statusLink]]" class="row">
- <div class="title">Status</div>
- <div>
- <a href="[[_convertUndefined(run.statusLink)]]" target="_blank"
- ><iron-icon
- aria-label="external link to check status"
- class="small link"
- icon="gr-icons:launch"
- ></iron-icon
- >[[computeHostName(run.statusLink)]]
- </a>
- </div>
- </div>
- <div hidden$="[[!run.statusDescription]]" class="row">
- <div class="title">Message</div>
- <div>[[run.statusDescription]]</div>
- </div>
- </div>
- </div>
- <div class="section" hidden$="[[hideAttempts(run)]]">
- <div class="sectionIcon">
- <iron-icon class="small" icon="gr-icons:arrow-forward"></iron-icon>
- </div>
- <div class="sectionContent">
- <div hidden$="[[hideAttempts(run)]]" class="attempts row">
- <div class="title">Attempt</div>
- <template is="dom-repeat" items="[[computeAttempts(run)]]">
- <div>
- <div class="attemptIcon">
- <iron-icon
- class$="[[item.icon]]"
- icon="gr-icons:[[item.icon]]"
- ></iron-icon>
- </div>
- <div class="attemptNumber">[[computeAttempt(item.attempt)]]</div>
- </div>
- </template>
- </div>
- </div>
- </div>
- <div class="section" hidden$="[[hideTimestampSection(run)]]">
- <div class="sectionIcon">
- <iron-icon class="small" icon="gr-icons:schedule"></iron-icon>
- </div>
- <div class="sectionContent">
- <div hidden$="[[hideScheduled(run)]]" class="row">
- <div class="title">Scheduled</div>
- <div>[[computeDuration(run.scheduledTimestamp)]]</div>
- </div>
- <div hidden$="[[!run.startedTimestamp]]" class="row">
- <div class="title">Started</div>
- <div>[[computeDuration(run.startedTimestamp)]]</div>
- </div>
- <div hidden$="[[!run.finishedTimestamp]]" class="row">
- <div class="title">Ended</div>
- <div>[[computeDuration(run.finishedTimestamp)]]</div>
- </div>
- <div hidden$="[[hideCompletion(run)]]" class="row">
- <div class="title">Completion</div>
- <div>[[computeCompletionDuration(run)]]</div>
- </div>
- </div>
- </div>
- <div class="section" hidden$="[[hideDescriptionSection(run)]]">
- <div class="sectionIcon">
- <iron-icon class="small" icon="gr-icons:link"></iron-icon>
- </div>
- <div class="sectionContent">
- <div hidden$="[[!run.checkDescription]]" class="row">
- <div class="title">Description</div>
- <div>[[run.checkDescription]]</div>
- </div>
- <div hidden$="[[!run.checkLink]]" class="row">
- <div class="title">Documentation</div>
- <div>
- <a href="[[_convertUndefined(run.checkLink)]]" target="_blank"
- ><iron-icon
- aria-label="external link to check documentation"
- class="small link"
- icon="gr-icons:launch"
- ></iron-icon
- >[[computeHostName(run.checkLink)]]
- </a>
- </div>
- </div>
- </div>
- </div>
- <template is="dom-repeat" items="[[computeActions(run)]]">
- <div class="action">
- <gr-checks-action
- event-target="[[_target]]"
- action="[[item]]"
- ></gr-checks-action>
- </div>
- </template>
- </div>
-`;
diff --git a/polygerrit-ui/app/elements/checks/gr-hovercard-run_test.ts b/polygerrit-ui/app/elements/checks/gr-hovercard-run_test.ts
index 67781f5..352219a 100644
--- a/polygerrit-ui/app/elements/checks/gr-hovercard-run_test.ts
+++ b/polygerrit-ui/app/elements/checks/gr-hovercard-run_test.ts
@@ -33,7 +33,7 @@
});
teardown(() => {
- element.hide();
+ element.hide(new MouseEvent('click'));
});
test('hovercard is shown', () => {
diff --git a/polygerrit-ui/app/elements/core/gr-account-dropdown/gr-account-dropdown.ts b/polygerrit-ui/app/elements/core/gr-account-dropdown/gr-account-dropdown.ts
index 0dbe823a..eb55177 100644
--- a/polygerrit-ui/app/elements/core/gr-account-dropdown/gr-account-dropdown.ts
+++ b/polygerrit-ui/app/elements/core/gr-account-dropdown/gr-account-dropdown.ts
@@ -76,12 +76,13 @@
super.disconnectedCallback();
}
- static get styles() {
+ static override get styles() {
return [
sharedStyles,
css`
gr-dropdown {
padding: 0 var(--spacing-m);
+ --gr-button-text-color: var(--header-text-color);
}
gr-avatar {
height: 2em;
@@ -92,15 +93,13 @@
];
}
- render() {
+ override render() {
// To pass CSS mixins for @apply to Polymer components, they need to appear
// in <style> inside the template.
+ /* eslint-disable lit/prefer-static-styles */
const customStyle = html`
<style>
gr-dropdown {
- --gr-button: {
- color: var(--header-text-color);
- }
--gr-dropdown-item: {
color: var(--primary-text-color);
}
diff --git a/polygerrit-ui/app/elements/core/gr-error-dialog/gr-error-dialog_test.ts b/polygerrit-ui/app/elements/core/gr-error-dialog/gr-error-dialog_test.ts
index 2dec8d2..c51988e 100644
--- a/polygerrit-ui/app/elements/core/gr-error-dialog/gr-error-dialog_test.ts
+++ b/polygerrit-ui/app/elements/core/gr-error-dialog/gr-error-dialog_test.ts
@@ -26,15 +26,16 @@
suite('gr-error-dialog tests', () => {
let element: GrErrorDialog;
- setup(() => {
+ setup(async () => {
element = basicFixture.instantiate();
+ await flush();
});
test('dismiss tap fires event', async () => {
const dismissCalled = mockPromise();
element.addEventListener('dismiss', () => dismissCalled.resolve());
MockInteractions.tap(
- (queryAndAssert(element, '#dialog') as GrDialog).$.confirm
+ (queryAndAssert(element, '#dialog') as GrDialog).confirmButton!
);
await dismissCalled;
});
diff --git a/polygerrit-ui/app/elements/core/gr-keyboard-shortcuts-dialog/gr-keyboard-shortcuts-dialog.ts b/polygerrit-ui/app/elements/core/gr-keyboard-shortcuts-dialog/gr-keyboard-shortcuts-dialog.ts
index 0b191f1..541d877 100644
--- a/polygerrit-ui/app/elements/core/gr-keyboard-shortcuts-dialog/gr-keyboard-shortcuts-dialog.ts
+++ b/polygerrit-ui/app/elements/core/gr-keyboard-shortcuts-dialog/gr-keyboard-shortcuts-dialog.ts
@@ -27,6 +27,7 @@
SectionView,
} from '../../../mixins/keyboard-shortcut-mixin/keyboard-shortcut-mixin';
import {property, customElement} from '@polymer/decorators';
+import {appContext} from '../../../services/app-context';
declare global {
interface HTMLElementTagNameMap {
@@ -60,13 +61,14 @@
@property({type: Array})
_right?: SectionShortcut[];
- private keyboardShortcutDirectoryListener: ShortcutListener;
+ private readonly shortcutListener: ShortcutListener;
+
+ private readonly shortcuts = appContext.shortcutsService;
constructor() {
super();
- this.keyboardShortcutDirectoryListener = (
- d?: Map<ShortcutSection, SectionView>
- ) => this._onDirectoryUpdated(d);
+ this.shortcutListener = (d?: Map<ShortcutSection, SectionView>) =>
+ this._onDirectoryUpdated(d);
}
override ready() {
@@ -76,15 +78,11 @@
override connectedCallback() {
super.connectedCallback();
- this.addKeyboardShortcutDirectoryListener(
- this.keyboardShortcutDirectoryListener
- );
+ this.shortcuts.addListener(this.shortcutListener);
}
override disconnectedCallback() {
- this.removeKeyboardShortcutDirectoryListener(
- this.keyboardShortcutDirectoryListener
- );
+ this.shortcuts.removeListener(this.shortcutListener);
super.disconnectedCallback();
}
diff --git a/polygerrit-ui/app/elements/core/gr-router/gr-router.ts b/polygerrit-ui/app/elements/core/gr-router/gr-router.ts
index 4a6f009..f0cff85 100644
--- a/polygerrit-ui/app/elements/core/gr-router/gr-router.ts
+++ b/polygerrit-ui/app/elements/core/gr-router/gr-router.ts
@@ -177,8 +177,8 @@
CHANGE_ID_QUERY: /^\/id\/(I[0-9a-f]{40})$/,
- // Matches /c/<changeNum>/[<basePatchNum>..][<patchNum>][/].
- CHANGE_LEGACY: /^\/c\/(\d+)\/?(((-?\d+|edit)(\.\.(\d+|edit))?))?\/?$/,
+ // Matches /c/<changeNum>/[*][/].
+ CHANGE_LEGACY: /^\/c\/(\d+)\/?(.*)$/,
CHANGE_NUMBER_LEGACY: /^\/(\d+)\/?/,
// Matches
@@ -210,10 +210,6 @@
// Matches /c/<project>/+/<changeNum>/[<patchNum|edit>]/<path>,edit[#lineNum]
DIFF_EDIT: /^\/c\/(.+)\/\+\/(\d+)\/(\d+|edit)\/(.+),edit(#\d+)?$/,
- // Matches non-project-relative
- // /c/<changeNum>/[<basePatchNum>..]<patchNum>/<path>.
- DIFF_LEGACY: /^\/c\/(\d+)\/((-?\d+|edit)(\.\.(\d+|edit))?)\/(.+)/,
-
// Matches diff routes using @\d+ to specify a file name (whether or not
// the project name is included).
// eslint-disable-next-line max-len
@@ -287,15 +283,6 @@
type QueryStringItem = [string, string]; // [key, value]
-type GenerateUrlLegacyChangeViewParameters = Omit<
- GenerateUrlChangeViewParameters,
- 'project'
->;
-type GenerateUrlLegacyDiffViewParameters = Omit<
- GenerateUrlDiffViewParameters,
- 'project'
->;
-
interface PatchRangeParams {
patchNum?: PatchSetNum;
basePatchNum?: BasePatchSetNum;
@@ -679,37 +666,6 @@
}
/**
- * Given a set of params without a project, gets the project from the rest
- * API project lookup and then sets the app params.
- */
- _normalizeLegacyRouteParams(
- params: Readonly<
- | GenerateUrlLegacyChangeViewParameters
- | GenerateUrlLegacyDiffViewParameters
- >
- ) {
- if (!params.changeNum) {
- return Promise.resolve();
- }
-
- return this.restApiService
- .getFromProjectLookup(params.changeNum)
- .then(project => {
- // Show a 404 and terminate if the lookup request failed. Attempting
- // to redirect after failing to get the project loops infinitely.
- if (!project) {
- this._show404();
- return;
- }
- const updatedParams:
- | GenerateUrlChangeViewParameters
- | GenerateUrlDiffViewParameters = {...params, project};
- this._normalizePatchRangeParams(updatedParams);
- this._redirect(this._generateUrl(updatedParams));
- });
- }
-
- /**
* Normalizes the params object, and determines if the URL needs to be
* modified to fit the proper schema.
*
@@ -1100,8 +1056,6 @@
this._mapRoute(RoutePattern.CHANGE_LEGACY, '_handleChangeLegacyRoute');
- this._mapRoute(RoutePattern.DIFF_LEGACY, '_handleDiffLegacyRoute');
-
this._mapRoute(RoutePattern.AGREEMENTS, '_handleAgreementsRoute', true);
this._mapRoute(
@@ -1666,41 +1620,26 @@
}
_handleChangeLegacyRoute(ctx: PageContextWithQueryMap) {
- // Parameter order is based on the regex group number matched.
- const params: GenerateUrlLegacyChangeViewParameters = {
- changeNum: Number(ctx.params[0]) as NumericChangeId,
- basePatchNum: convertToPatchSetNum(ctx.params[3]) as BasePatchSetNum,
- patchNum: convertToPatchSetNum(ctx.params[5]),
- view: GerritView.CHANGE,
- querystring: ctx.querystring,
- };
-
- this._normalizeLegacyRouteParams(params);
+ const changeNum = Number(ctx.params[0]) as NumericChangeId;
+ if (!changeNum) {
+ this._show404();
+ return;
+ }
+ this.restApiService.getFromProjectLookup(changeNum).then(project => {
+ // Show a 404 and terminate if the lookup request failed. Attempting
+ // to redirect after failing to get the project loops infinitely.
+ if (!project) {
+ this._show404();
+ return;
+ }
+ this._redirect(`/c/${project}/+/${changeNum}/${ctx.params[1]}`);
+ });
}
_handleLegacyLinenum(ctx: PageContextWithQueryMap) {
this._redirect(ctx.path.replace(LEGACY_LINENUM_PATTERN, '#$1'));
}
- _handleDiffLegacyRoute(ctx: PageContextWithQueryMap) {
- // Parameter order is based on the regex group number matched.
- const params: GenerateUrlLegacyDiffViewParameters = {
- changeNum: Number(ctx.params[0]) as NumericChangeId,
- basePatchNum: convertToPatchSetNum(ctx.params[2]) as BasePatchSetNum,
- patchNum: convertToPatchSetNum(ctx.params[4]),
- path: ctx.params[5],
- view: GerritView.DIFF,
- };
-
- const address = this._parseLineAddress(ctx.hash);
- if (address) {
- params.leftSide = address.leftSide;
- params.lineNum = address.lineNum;
- }
-
- this._normalizeLegacyRouteParams(params);
- }
-
_handleDiffEditRoute(ctx: PageContextWithQueryMap) {
// Parameter order is based on the regex group number matched.
const project = ctx.params[0] as RepoName;
diff --git a/polygerrit-ui/app/elements/core/gr-router/gr-router_test.js b/polygerrit-ui/app/elements/core/gr-router/gr-router_test.js
index 4c7855b..b91bf0c 100644
--- a/polygerrit-ui/app/elements/core/gr-router/gr-router_test.js
+++ b/polygerrit-ui/app/elements/core/gr-router/gr-router_test.js
@@ -192,7 +192,6 @@
'_handleDiffRoute',
'_handleDefaultRoute',
'_handleChangeLegacyRoute',
- '_handleDiffLegacyRoute',
'_handleDocumentationRedirectRoute',
'_handleDocumentationSearchRoute',
'_handleDocumentationSearchRedirectRoute',
@@ -536,66 +535,6 @@
});
suite('param normalization', () => {
- let projectLookupStub;
- let generateUrlStub;
-
- setup(() => {
- projectLookupStub = stubRestApi('getFromProjectLookup');
- generateUrlStub = sinon.stub(element, '_generateUrl');
- });
-
- suite('_normalizeLegacyRouteParams', () => {
- let rangeStub;
- let redirectStub;
- let show404Stub;
-
- setup(() => {
- rangeStub = sinon.stub(element, '_normalizePatchRangeParams')
- .returns(Promise.resolve());
- redirectStub = sinon.stub(element, '_redirect');
- show404Stub = sinon.stub(element, '_show404');
- });
-
- test('w/o changeNum', () => {
- projectLookupStub.returns(Promise.resolve('foo/bar'));
- const params = {};
- return element._normalizeLegacyRouteParams(params).then(() => {
- assert.isFalse(generateUrlStub.calledOnce);
- assert.isFalse(projectLookupStub.called);
- assert.isFalse(rangeStub.called);
- assert.isFalse(redirectStub.called);
- assert.isFalse(show404Stub.called);
- });
- });
-
- test('w/ changeNum', () => {
- projectLookupStub.returns(Promise.resolve('foo/bar'));
- const params = {changeNum: 1234};
-
- return element._normalizeLegacyRouteParams(params).then(() => {
- assert.isTrue(generateUrlStub.calledOnce);
- const updatedParams = generateUrlStub.lastCall.args[0];
- assert.isTrue(projectLookupStub.called);
- assert.isTrue(rangeStub.called);
- assert.equal(updatedParams.project, 'foo/bar');
- assert.isTrue(redirectStub.calledOnce);
- assert.isFalse(show404Stub.called);
- });
- });
-
- test('halts on project lookup failure', () => {
- projectLookupStub.returns(Promise.resolve(undefined));
- const params = {changeNum: 1234};
- return element._normalizeLegacyRouteParams(params).then(() => {
- assert.isFalse(generateUrlStub.calledOnce);
- assert.isTrue(projectLookupStub.called);
- assert.isFalse(rangeStub.called);
- assert.isFalse(redirectStub.called);
- assert.isTrue(show404Stub.calledOnce);
- });
- });
- });
-
suite('_normalizePatchRangeParams', () => {
test('range n..n normalizes to n', () => {
const params = {basePatchNum: 4, patchNum: 4};
@@ -1367,58 +1306,19 @@
assert.isTrue(redirectStub.calledWithExactly('/c/12345'));
});
- test('_handleChangeLegacyRoute', () => {
- const normalizeRouteStub = sinon.stub(element,
- '_normalizeLegacyRouteParams');
+ test('_handleChangeLegacyRoute', async () => {
+ stubRestApi('getFromProjectLookup').returns(Promise.resolve('project'));
const ctx = {
params: [
1234, // 0 Change number
- null, // 1 Unused
- null, // 2 Unused
- 6, // 3 Base patch number
- null, // 4 Unused
- 9, // 5 Patch number
+ 'comment/6789',
],
querystring: '',
};
element._handleChangeLegacyRoute(ctx);
- assert.isTrue(normalizeRouteStub.calledOnce);
- assert.deepEqual(normalizeRouteStub.lastCall.args[0], {
- changeNum: 1234,
- basePatchNum: 6,
- patchNum: 9,
- view: GerritView.CHANGE,
- querystring: '',
- });
- });
-
- test('_handleDiffLegacyRoute', () => {
- const normalizeRouteStub = sinon.stub(element,
- '_normalizeLegacyRouteParams');
- const ctx = {
- params: [
- 1234, // 0 Change number
- null, // 1 Unused
- 3, // 2 Base patch number
- null, // 3 Unused
- 8, // 4 Patch number
- 'foo/bar', // 5 Diff path
- ],
- path: '/c/1234/3..8/foo/bar',
- hash: 'b123',
- };
- element._handleDiffLegacyRoute(ctx);
- assert.isFalse(redirectStub.called);
- assert.isTrue(normalizeRouteStub.calledOnce);
- assert.deepEqual(normalizeRouteStub.lastCall.args[0], {
- changeNum: 1234,
- basePatchNum: 3,
- patchNum: 8,
- view: GerritView.DIFF,
- path: 'foo/bar',
- lineNum: 123,
- leftSide: true,
- });
+ await flush();
+ assert.isTrue(redirectStub.calledWithExactly('/c/project/+/1234' +
+ '/comment/6789'));
});
test('_handleLegacyLinenum w/ @321', () => {
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 7b12b1117..aaeb924 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
@@ -31,10 +31,9 @@
GrAutocomplete,
} from '../../shared/gr-autocomplete/gr-autocomplete';
import {getDocsBaseUrl} from '../../../utils/url-util';
-import {CustomKeyboardEvent} from '../../../types/events';
+import {IronKeyboardEvent} from '../../../types/events';
import {MergeabilityComputationBehavior} from '../../../constants/constants';
import {appContext} from '../../../services/app-context';
-import {getKeyboardEvent} from '../../../utils/dom-util';
// Possible static search options for auto complete, without negations.
const SEARCH_OPERATORS: ReadonlyArray<string> = [
@@ -56,7 +55,6 @@
'commentby:',
'commit:',
'committer:',
- 'conflicts:',
'deleted:',
'delta:',
'dir:',
@@ -199,12 +197,14 @@
private readonly restApiService = appContext.restApiService;
+ private readonly shortcuts = appContext.shortcutsService;
+
constructor() {
super();
this.query = (input: string) => this._getSearchSuggestions(input);
}
- connectedCallback() {
+ override connectedCallback() {
super.connectedCallback();
this.restApiService.getConfig().then((serverConfig?: ServerInfo) => {
const mergeability =
@@ -396,10 +396,10 @@
});
}
- _handleSearch(e: CustomKeyboardEvent) {
- const keyboardEvent = getKeyboardEvent(e);
+ _handleSearch(e: IronKeyboardEvent) {
+ const keyboardEvent = e.detail.keyboardEvent;
if (
- this.shouldSuppressKeyboardShortcut(e) ||
+ this.shortcuts.shouldSuppress(e) ||
(this.modifierPressed(e) && !keyboardEvent.shiftKey)
) {
return;
diff --git a/polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar_test.ts b/polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar_test.ts
index b5b0124..b6d0579 100644
--- a/polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar_test.ts
+++ b/polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar_test.ts
@@ -19,12 +19,7 @@
import './gr-search-bar';
import '../../../scripts/util';
import {GrSearchBar} from './gr-search-bar';
-import {
- TestKeyboardShortcutBinder,
- stubRestApi,
- mockPromise,
-} from '../../../test/test-utils';
-import {Shortcut} from '../../../mixins/keyboard-shortcut-mixin/keyboard-shortcut-mixin';
+import {stubRestApi, mockPromise} from '../../../test/test-utils';
import {_testOnly_clearDocsBaseUrlCache} from '../../../utils/url-util';
import * as MockInteractions from '@polymer/iron-test-helpers/mock-interactions';
import {
@@ -39,15 +34,6 @@
suite('gr-search-bar tests', () => {
let element: GrSearchBar;
- suiteSetup(() => {
- const kb = TestKeyboardShortcutBinder.push();
- kb.bindShortcut(Shortcut.SEARCH, '/');
- });
-
- suiteTeardown(() => {
- TestKeyboardShortcutBinder.pop();
- });
-
setup(async () => {
element = basicFixture.instantiate();
await flush();
diff --git a/polygerrit-ui/app/elements/diff/gr-apply-fix-dialog/gr-apply-fix-dialog.ts b/polygerrit-ui/app/elements/diff/gr-apply-fix-dialog/gr-apply-fix-dialog.ts
index 1c25755..f5072b9 100644
--- a/polygerrit-ui/app/elements/diff/gr-apply-fix-dialog/gr-apply-fix-dialog.ts
+++ b/polygerrit-ui/app/elements/diff/gr-apply-fix-dialog/gr-apply-fix-dialog.ts
@@ -150,7 +150,7 @@
});
}
- connectedCallback() {
+ override connectedCallback() {
super.connectedCallback();
this.refitOverlay = () => {
// re-center the dialog as content changed
diff --git a/polygerrit-ui/app/elements/diff/gr-apply-fix-dialog/gr-apply-fix-dialog_test.ts b/polygerrit-ui/app/elements/diff/gr-apply-fix-dialog/gr-apply-fix-dialog_test.ts
index d3d7615..94d37f5 100644
--- a/polygerrit-ui/app/elements/diff/gr-apply-fix-dialog/gr-apply-fix-dialog_test.ts
+++ b/polygerrit-ui/app/elements/diff/gr-apply-fix-dialog/gr-apply-fix-dialog_test.ts
@@ -186,6 +186,7 @@
})
);
element._isApplyFixLoading = true;
+ await flush();
const button = getConfirmButton();
assert.isTrue(button.hasAttribute('disabled'));
assert.equal(button.getAttribute('title'), '');
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-builder/token-highlight-layer.ts b/polygerrit-ui/app/elements/diff/gr-diff-builder/token-highlight-layer.ts
index 480e26c..de7d007 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-builder/token-highlight-layer.ts
+++ b/polygerrit-ui/app/elements/diff/gr-diff-builder/token-highlight-layer.ts
@@ -15,9 +15,17 @@
* limitations under the License.
*/
import {DiffLayer, DiffLayerListener} from '../../../types/types';
-import {GrDiffLine, Side, TokenHighlightedListener} from '../../../api/diff';
+import {GrDiffLine, Side, TokenHighlightListener} from '../../../api/diff';
+import {assertIsDefined} from '../../../utils/common-util';
import {GrAnnotation} from '../gr-diff-highlight/gr-annotation';
import {debounce, DelayedTask} from '../../../utils/async-util';
+
+import {
+ getLineElByChild,
+ getSideByLineEl,
+ getPreviousContentNodes,
+} from '../gr-diff/gr-diff-utils';
+
import {
getLineNumberByChild,
lineNumberToNumber,
@@ -66,7 +74,7 @@
private currentHighlight?: string;
/** Trigger when a new token starts or stoped being highlighted.*/
- private readonly tokenHighlightedListener?: TokenHighlightedListener;
+ private readonly tokenHighlightListener?: TokenHighlightListener;
/**
* The line of the currently highlighted token. We store this in order to
@@ -100,9 +108,9 @@
constructor(
container: HTMLElement = document.documentElement,
- tokenHighlightedListener?: TokenHighlightedListener
+ tokenHighlightListener?: TokenHighlightListener
) {
- this.tokenHighlightedListener = tokenHighlightedListener;
+ this.tokenHighlightListener = tokenHighlightListener;
container.addEventListener('click', e => {
this.handleContainerClick(e);
});
@@ -260,18 +268,42 @@
const oldLineNumber = this.currentHighlightLineNumber;
this.currentHighlight = newHighlight;
this.currentHighlightLineNumber = newLineNumber;
-
- if (this.tokenHighlightedListener) {
- this.tokenHighlightedListener(
- newHighlight,
- newLineNumber,
- newHoveredElement
- );
- }
+ this.triggerTokenHighlightEvent(
+ newHighlight,
+ newLineNumber,
+ newHoveredElement
+ );
this.notifyForToken(oldHighlight, oldLineNumber);
this.notifyForToken(newHighlight, newLineNumber);
}
+ triggerTokenHighlightEvent(
+ token: string | undefined,
+ line: number,
+ element: Element | undefined
+ ) {
+ if (!this.tokenHighlightListener) {
+ return;
+ }
+ if (!token || !element) {
+ this.tokenHighlightListener(undefined);
+ return;
+ }
+ const previousTextLength = getPreviousContentNodes(element)
+ .map(sib => sib.textContent!.length)
+ .reduce((partial_sum, a) => partial_sum + a, 0);
+ const lineEl = getLineElByChild(element);
+ assertIsDefined(lineEl, 'Line element should be found!');
+ const side = getSideByLineEl(lineEl);
+ const range = {
+ start_line: line,
+ start_column: previousTextLength + 1, // 1-based inclusive
+ end_line: line,
+ end_column: previousTextLength + token.length, // 1-based inclusive
+ };
+ this.tokenHighlightListener({token, element, side, range});
+ }
+
getSortedLinesForSide(
lineMapping: Map<string, Set<number>>,
token: string | undefined,
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-builder/token-highlight-layer_test.ts b/polygerrit-ui/app/elements/diff/gr-diff-builder/token-highlight-layer_test.ts
index 4f44665..2993d35 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-builder/token-highlight-layer_test.ts
+++ b/polygerrit-ui/app/elements/diff/gr-diff-builder/token-highlight-layer_test.ts
@@ -17,7 +17,7 @@
import '../../../test/common-test-setup-karma';
import * as MockInteractions from '@polymer/iron-test-helpers/mock-interactions';
-import {Side} from '../../../api/diff';
+import {Side, TokenHighlightEventDetails} from '../../../api/diff';
import {GrDiffLine, GrDiffLineType} from '../gr-diff/gr-diff-line.js';
import {HOVER_DELAY_MS, TokenHighlightLayer} from './token-highlight-layer';
import {GrAnnotation} from '../gr-diff-highlight/gr-annotation';
@@ -66,14 +66,12 @@
let container: HTMLElement;
let listener: MockListener;
let highlighter: TokenHighlightLayer;
- let tokenHighlightingCalls: any[] = [];
+ let tokenHighlightingCalls: {details?: TokenHighlightEventDetails}[] = [];
- function tokenHighlightedListener(
- newHighlight: string | undefined,
- newLineNumber: number,
- hoveredElement?: Element
+ function tokenHighlightListener(
+ highlightDetails?: TokenHighlightEventDetails
) {
- tokenHighlightingCalls.push({newHighlight, newLineNumber, hoveredElement});
+ tokenHighlightingCalls.push({details: highlightDetails});
}
setup(async () => {
@@ -81,7 +79,7 @@
tokenHighlightingCalls = [];
container = document.createElement('div');
document.body.appendChild(container);
- highlighter = new TokenHighlightLayer(container, tokenHighlightedListener);
+ highlighter = new TokenHighlightLayer(container, tokenHighlightListener);
highlighter.addListener((...args) => listener.notify(...args));
});
@@ -107,10 +105,13 @@
const lineId = createLineId();
const template = html`
<div class="line">
- <div data-value=${line} class="lineNum"></div>
- <div id=${lineId} class="line-content">${text}</div>
+ <div data-value=${line} class="lineNum right"></div>
+ <div class="content">
+ <div id=${lineId} class="contentText">${text}</div>
+ </div>
</div>
`;
+
const div = document.createElement('div');
render(template, div);
container.appendChild(div);
@@ -277,19 +278,16 @@
assert.equal(tokenHighlightingCalls.length, 0);
clock.tick(HOVER_DELAY_MS);
assert.equal(tokenHighlightingCalls.length, 1);
- assert.deepEqual(tokenHighlightingCalls[0], {
- newHighlight: 'words',
- newLineNumber: 1,
- hoveredElement: words1,
+ assert.deepEqual(tokenHighlightingCalls[0].details, {
+ token: 'words',
+ side: Side.RIGHT,
+ element: words1,
+ range: {start_line: 1, start_column: 5, end_line: 1, end_column: 9},
});
MockInteractions.click(container);
assert.equal(tokenHighlightingCalls.length, 2);
- assert.deepEqual(tokenHighlightingCalls[1], {
- newHighlight: undefined,
- newLineNumber: 0,
- hoveredElement: undefined,
- });
+ assert.deepEqual(tokenHighlightingCalls[1].details, undefined);
});
test('clicking clears highlight', async () => {
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-annotation.ts b/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-annotation.ts
index 5e81871..79e7dbc 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-annotation.ts
+++ b/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-annotation.ts
@@ -134,7 +134,7 @@
if (node instanceof Text) {
this._annotateText(node, offset, subLength, cssClass);
- } else if (node instanceof HTMLElement) {
+ } else if (node instanceof Element) {
this.annotateElement(node, offset, subLength, cssClass);
}
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host.ts b/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host.ts
index 463163c..b1bad1c 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host.ts
+++ b/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host.ts
@@ -56,6 +56,7 @@
PatchRange,
PatchSetNum,
RepoName,
+ UrlEncodedCommentId,
} from '../../../types/common';
import {
DiffInfo,
@@ -223,9 +224,6 @@
@property({type: Boolean})
_loggedIn = false;
- @property({type: Boolean})
- disableTokenHighlighting = false;
-
@property({type: String})
_errorMessage: string | null = null;
@@ -303,11 +301,6 @@
this.addEventListener('diff-context-expanded', event =>
this._handleDiffContextExpanded(event)
);
- appContext.restApiService.getPreferences().then(prefs => {
- if (prefs?.disable_token_highlighting) {
- this.disableTokenHighlighting = prefs.disable_token_highlighting;
- }
- });
}
override ready() {
@@ -335,18 +328,21 @@
super.disconnectedCallback();
}
- initLayers() {
- return getPluginLoader()
- .awaitPluginsLoaded()
- .then(() => {
- assertIsDefined(this.path, 'path');
- this._layers = this._getLayers(this.path);
- this._coverageRanges = [];
- // We kick off fetching the data here, but we don't return the promise,
- // so awaiting initLayers() will not wait for coverage data to be
- // completely loaded.
- this._getCoverageData();
- });
+ async initLayers() {
+ const preferencesPromise = appContext.restApiService.getPreferences();
+ await getPluginLoader().awaitPluginsLoaded();
+ const prefs = await preferencesPromise;
+ const enableTokenHighlight =
+ appContext.flagsService.isEnabled(KnownExperimentId.TOKEN_HIGHLIGHTING) &&
+ !prefs?.disable_token_highlighting;
+
+ assertIsDefined(this.path, 'path');
+ this._layers = this.getLayers(this.path, enableTokenHighlight);
+ this._coverageRanges = [];
+ // We kick off fetching the data here, but we don't return the promise,
+ // so awaiting initLayers() will not wait for coverage data to be
+ // completely loaded.
+ this._getCoverageData();
}
diffChanged(diff?: DiffInfo) {
@@ -418,12 +414,9 @@
}
}
- private _getLayers(path: string): DiffLayer[] {
+ private getLayers(path: string, enableTokenHighlight: boolean): DiffLayer[] {
const layers = [];
- if (
- appContext.flagsService.isEnabled(KnownExperimentId.TOKEN_HIGHLIGHTING) &&
- !this.disableTokenHighlighting
- ) {
+ if (enableTokenHighlight) {
layers.push(new TokenHighlightLayer(this));
}
layers.push(this.syntaxLayer);
@@ -738,9 +731,24 @@
}
_threadsChanged(threads: CommentThread[]) {
- const threadEls = new Set<Object>();
+ const threadEls = new Set<GrCommentThread>();
+ const rootIdToThreadEl = new Map<UrlEncodedCommentId, GrCommentThread>();
+ for (const threadEl of this.getThreadEls()) {
+ if (threadEl.rootId) {
+ rootIdToThreadEl.set(threadEl.rootId, threadEl);
+ }
+ }
for (const thread of threads) {
- threadEls.add(this._getOrCreateThread(thread));
+ const existingThreadEl =
+ thread.rootId && rootIdToThreadEl.get(thread.rootId);
+ if (existingThreadEl) {
+ this._updateThreadElement(existingThreadEl, thread);
+ threadEls.add(existingThreadEl);
+ } else {
+ const threadEl = this._createThreadElement(thread);
+ this._attachThreadElement(threadEl);
+ threadEls.add(threadEl);
+ }
}
// Remove all threads that are no longer existing.
for (const threadEl of this.getThreadEls()) {
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host_test.js b/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host_test.js
index 344f9d8..8901636 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host_test.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host_test.js
@@ -17,19 +17,16 @@
import '../../../test/common-test-setup-karma.js';
import './gr-diff-host.js';
-import {GrDiffBuilderImage} from '../gr-diff-builder/gr-diff-builder-image.js';
-import {GerritNav} from '../../core/gr-navigation/gr-navigation.js';
+
import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js';
-import {Side, createDefaultDiffPrefs} from '../../../constants/constants.js';
-import {createChange} from '../../../test/test-data-generators.js';
-import {CoverageType} from '../../../types/types.js';
-import {
- addListenerForTest,
- mockPromise,
- stubRestApi,
-} from '../../../test/test-utils.js';
-import {EditPatchSetNum, ParentPatchSetNum} from '../../../types/common.js';
+import {createDefaultDiffPrefs, Side} from '../../../constants/constants.js';
import {_testOnly_resetState} from '../../../services/comments/comments-model.js';
+import {createChange, createComment, createCommentThread} from '../../../test/test-data-generators.js';
+import {addListenerForTest, mockPromise, stubRestApi} from '../../../test/test-utils.js';
+import {EditPatchSetNum, ParentPatchSetNum} from '../../../types/common.js';
+import {CoverageType} from '../../../types/types.js';
+import {GerritNav} from '../../core/gr-navigation/gr-navigation.js';
+import {GrDiffBuilderImage} from '../gr-diff-builder/gr-diff-builder-image.js';
const basicFixture = fixtureFromElement('gr-diff-host');
@@ -1127,6 +1124,43 @@
assert.equal(threads[0].path, element.file.path);
});
+ test('multiple threads created on the same range', () => {
+ element.patchRange = {
+ basePatchNum: 2,
+ patchNum: 3,
+ };
+ element.file = {basePath: 'file_renamed.txt', path: element.path};
+
+ const comment = createComment();
+ comment.range = {
+ start_line: 1,
+ start_character: 1,
+ end_line: 2,
+ end_character: 2,
+ };
+ const thread = createCommentThread([comment]);
+ element.threads = [thread];
+
+ let threads = dom(element.$.diff)
+ .queryDistributedElements('gr-comment-thread');
+
+ assert.equal(threads.length, 1);
+ element.threads= [...element.threads, thread];
+
+ threads = dom(element.$.diff)
+ .queryDistributedElements('gr-comment-thread');
+ // Threads have same rootId so element is reused
+ assert.equal(threads.length, 1);
+
+ const newThread = {...thread};
+ newThread.rootId = 'differentRootId';
+ element.threads= [...element.threads, newThread];
+ threads = dom(element.$.diff)
+ .queryDistributedElements('gr-comment-thread');
+ // New thread has a different rootId
+ assert.equal(threads.length, 2);
+ });
+
test('thread should use new file path if first created' +
'on patch set (left) but is base', () => {
const diffSide = Side.LEFT;
@@ -1143,8 +1177,8 @@
},
}));
- const threads = dom(element.$.diff)
- .queryDistributedElements('gr-comment-thread');
+ const threads =
+ dom(element.$.diff).queryDistributedElements('gr-comment-thread');
assert.equal(threads.length, 1);
assert.equal(threads[0].diffSide, diffSide);
@@ -1167,8 +1201,8 @@
},
}));
- const threads = dom(element.$.diff)
- .queryDistributedElements('gr-comment-thread');
+ const threads =
+ dom(element.$.diff).queryDistributedElements('gr-comment-thread');
assert.equal(threads.length, 0);
assert.isTrue(alertSpy.called);
});
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-image-viewer/gr-image-viewer.ts b/polygerrit-ui/app/elements/diff/gr-diff-image-viewer/gr-image-viewer.ts
index ba1abd0..32f5f39 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-image-viewer/gr-image-viewer.ts
+++ b/polygerrit-ui/app/elements/diff/gr-diff-image-viewer/gr-image-viewer.ts
@@ -267,11 +267,11 @@
outline: 1px solid transparent;
border: 1px solid var(--primary-button-background-color);
}
- paper-button[unelevated] {
+ paper-button.unelevated {
color: var(--primary-button-text-color);
background-color: var(--primary-button-background-color);
}
- paper-button[outlined] {
+ paper-button.outlined {
color: var(--primary-button-background-color);
}
#version-switcher {
@@ -448,12 +448,20 @@
// This uses the unelevated and outlined attributes from mwc-button with
// manual styling, for a more seamless transition later.
+ const leftClasses = {
+ left: true,
+ unelevated: this.baseSelected,
+ outlined: !this.baseSelected,
+ };
+ const rightClasses = {
+ right: true,
+ unelevated: !this.baseSelected,
+ outlined: this.baseSelected,
+ };
const versionToggle = html`
<div id="version-switcher">
<paper-button
- class="left"
- ?unelevated=${this.baseSelected}
- ?outlined=${!this.baseSelected}
+ class="${classMap(leftClasses)}"
@click="${this.selectBase}"
>
Base
@@ -461,9 +469,7 @@
<paper-fab mini icon="gr-icons:swapHoriz" @click="${this.manualBlink}">
</paper-fab>
<paper-button
- class="right"
- ?unelevated=${!this.baseSelected}
- ?outlined=${this.baseSelected}
+ class="${classMap(rightClasses)}"
@click="${this.selectRevision}"
>
Revision
@@ -509,7 +515,7 @@
<paper-listbox
slot="dropdown-content"
selected="fit"
- attr-for-selected="value"
+ .attrForSelected="${'value'}"
@selected-changed="${this.zoomControlChanged}"
>
${this.zoomLevels.map(
@@ -579,6 +585,7 @@
// To pass CSS mixins for @apply to Polymer components, they need to appear
// in <style> inside the template.
+ /* eslint-disable lit/prefer-static-styles */
const customStyle = html`
<style>
paper-item {
@@ -648,7 +655,9 @@
// We don't want property changes in updateSizes() to trigger infinite update
// loops, so we perform this in update() instead of updated().
override update(changedProperties: PropertyValues) {
+ // eslint-disable-next-line lit/no-property-change-update
if (!this.baseUrl) this.baseSelected = false;
+ // eslint-disable-next-line lit/no-property-change-update
if (!this.revisionUrl) this.baseSelected = true;
this.updateSizes();
super.update(changedProperties);
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-mode-selector/gr-diff-mode-selector_html.ts b/polygerrit-ui/app/elements/diff/gr-diff-mode-selector/gr-diff-mode-selector_html.ts
index 4e2b6a1..8a6d95d 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-mode-selector/gr-diff-mode-selector_html.ts
+++ b/polygerrit-ui/app/elements/diff/gr-diff-mode-selector/gr-diff-mode-selector_html.ts
@@ -30,28 +30,34 @@
width: 1.3rem;
}
</style>
- <gr-button
- id="sideBySideBtn"
- link=""
+ <gr-tooltip-content
has-tooltip=""
- position-below="[[showTooltipBelow]]"
- class$="[[_computeSideBySideSelected(mode)]]"
title="Side-by-side diff"
- aria-pressed$="[[isSideBySideSelected(mode)]]"
- on-click="_handleSideBySideTap"
+ position-below="[[showTooltipBelow]]"
>
- <iron-icon icon="gr-icons:side-by-side"></iron-icon>
- </gr-button>
- <gr-button
- id="unifiedBtn"
- link=""
+ <gr-button
+ id="sideBySideBtn"
+ link=""
+ class$="[[_computeSideBySideSelected(mode)]]"
+ aria-pressed$="[[isSideBySideSelected(mode)]]"
+ on-click="_handleSideBySideTap"
+ >
+ <iron-icon icon="gr-icons:side-by-side"></iron-icon>
+ </gr-button>
+ </gr-tooltip-content>
+ <gr-tooltip-content
has-tooltip=""
position-below="[[showTooltipBelow]]"
title="Unified diff"
- class$="[[_computeUnifiedSelected(mode)]]"
- aria-pressed$="[[isUnifiedSelected(mode)]]"
- on-click="_handleUnifiedTap"
>
- <iron-icon icon="gr-icons:unified"></iron-icon>
- </gr-button>
+ <gr-button
+ id="unifiedBtn"
+ link=""
+ class$="[[_computeUnifiedSelected(mode)]]"
+ aria-pressed$="[[isUnifiedSelected(mode)]]"
+ on-click="_handleUnifiedTap"
+ >
+ <iron-icon icon="gr-icons:unified"></iron-icon>
+ </gr-button>
+ </gr-tooltip-content>
`;
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.ts b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.ts
index b1d71cc..e2baec9 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.ts
+++ b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.ts
@@ -16,6 +16,7 @@
*/
import '@polymer/iron-dropdown/iron-dropdown';
import '@polymer/iron-input/iron-input';
+import '../../../styles/gr-a11y-styles';
import '../../../styles/shared-styles';
import '../../shared/gr-button/gr-button';
import '../../shared/gr-dropdown/gr-dropdown';
@@ -36,6 +37,7 @@
import {
KeyboardShortcutMixin,
Shortcut,
+ ShortcutSection,
} from '../../../mixins/keyboard-shortcut-mixin/keyboard-shortcut-mixin';
import {
GeneratedWebLink,
@@ -97,14 +99,15 @@
} from '../../../utils/comment-util';
import {AppElementParams} from '../../gr-app-types';
import {
- CustomKeyboardEvent,
+ IronKeyboardEventListener,
+ IronKeyboardEvent,
EventType,
OpenFixPreviewEvent,
} from '../../../types/events';
import {fireAlert, fireEvent, fireTitleChange} from '../../../utils/event-util';
import {GerritView} from '../../../services/router/router-model';
import {assertIsDefined} from '../../../utils/common-util';
-import {toggleClass, getKeyboardEvent} from '../../../utils/dom-util';
+import {toggleClass} from '../../../utils/dom-util';
import {CursorMoveResult} from '../../../api/core';
import {throttleWrap} from '../../../utils/async-util';
import {changeComments$} from '../../../services/comments/comments-model';
@@ -340,7 +343,9 @@
private readonly commentsService = appContext.commentsService;
- _throttledToggleFileReviewed?: EventListener;
+ private readonly shortcuts = appContext.shortcutsService;
+
+ _throttledToggleFileReviewed?: IronKeyboardEventListener;
_onRenderHandler?: EventListener;
@@ -351,7 +356,7 @@
override connectedCallback() {
super.connectedCallback();
this._throttledToggleFileReviewed = throttleWrap(e =>
- this._handleToggleFileReviewed(e as CustomKeyboardEvent)
+ this._handleToggleFileReviewed(e)
);
this._getLoggedIn().then(loggedIn => {
this._loggedIn = loggedIn;
@@ -517,38 +522,38 @@
);
}
- _handleToggleFileReviewed(e: CustomKeyboardEvent) {
- if (this.shouldSuppressKeyboardShortcut(e)) return;
+ _handleToggleFileReviewed(e: IronKeyboardEvent) {
+ if (this.shortcuts.shouldSuppress(e)) return;
if (this.modifierPressed(e)) return;
e.preventDefault();
this._setReviewed(!this.$.reviewed.checked);
}
- _handleEscKey(e: CustomKeyboardEvent) {
- if (this.shouldSuppressKeyboardShortcut(e)) return;
+ _handleEscKey(e: IronKeyboardEvent) {
+ if (this.shortcuts.shouldSuppress(e)) return;
if (this.modifierPressed(e)) return;
e.preventDefault();
this.$.diffHost.displayLine = false;
}
- _handleLeftPane(e: CustomKeyboardEvent) {
- if (this.shouldSuppressKeyboardShortcut(e)) return;
+ _handleLeftPane(e: IronKeyboardEvent) {
+ if (this.shortcuts.shouldSuppress(e)) return;
e.preventDefault();
this.cursor.moveLeft();
}
- _handleRightPane(e: CustomKeyboardEvent) {
- if (this.shouldSuppressKeyboardShortcut(e)) return;
+ _handleRightPane(e: IronKeyboardEvent) {
+ if (this.shortcuts.shouldSuppress(e)) return;
e.preventDefault();
this.cursor.moveRight();
}
- _handlePrevLineOrFileWithComments(e: CustomKeyboardEvent) {
- if (this.shouldSuppressKeyboardShortcut(e)) return;
+ _handlePrevLineOrFileWithComments(e: IronKeyboardEvent) {
+ if (this.shortcuts.shouldSuppress(e)) return;
if (
e.detail.keyboardEvent?.shiftKey &&
@@ -567,8 +572,8 @@
this.cursor.moveUp();
}
- _handleVisibleLine(e: CustomKeyboardEvent) {
- if (this.shouldSuppressKeyboardShortcut(e)) return;
+ _handleVisibleLine(e: IronKeyboardEvent) {
+ if (this.shortcuts.shouldSuppress(e)) return;
e.preventDefault();
this.cursor.moveToVisibleArea();
@@ -578,8 +583,8 @@
this.$.applyFixDialog.open(e);
}
- _handleNextLineOrFileWithComments(e: CustomKeyboardEvent) {
- if (this.shouldSuppressKeyboardShortcut(e)) return;
+ _handleNextLineOrFileWithComments(e: IronKeyboardEvent) {
+ if (this.shortcuts.shouldSuppress(e)) return;
if (
e.detail.keyboardEvent?.shiftKey &&
@@ -637,39 +642,41 @@
);
}
- _handleNewComment(e: CustomKeyboardEvent) {
- if (this.shouldSuppressKeyboardShortcut(e)) return;
- if (this.modifierPressed(e)) return;
+ _handleNewComment(ike: IronKeyboardEvent) {
+ if (this.shortcuts.shouldSuppress(ike)) return;
+ if (this.modifierPressed(ike)) return;
- e.preventDefault();
+ ike.preventDefault();
this.classList.remove('hideComments');
this.cursor.createCommentInPlace();
}
- _handlePrevFile(e: CustomKeyboardEvent) {
- if (this.shouldSuppressKeyboardShortcut(e)) return;
+ _handlePrevFile(ike: IronKeyboardEvent) {
+ const ke = ike.detail.keyboardEvent;
+ if (this.shortcuts.shouldSuppress(ike)) return;
// Check for meta key to avoid overriding native chrome shortcut.
- if (getKeyboardEvent(e).metaKey) return;
+ if (ke.metaKey) return;
if (!this._path) return;
if (!this._fileList) return;
- e.preventDefault();
+ ike.preventDefault();
this._navToFile(this._path, this._fileList, -1);
}
- _handleNextFile(e: CustomKeyboardEvent) {
- if (this.shouldSuppressKeyboardShortcut(e)) return;
+ _handleNextFile(ike: IronKeyboardEvent) {
+ const ke = ike.detail.keyboardEvent;
+ if (this.shortcuts.shouldSuppress(ike)) return;
// Check for meta key to avoid overriding native chrome shortcut.
- if (getKeyboardEvent(e).metaKey) return;
+ if (ke.metaKey) return;
if (!this._path) return;
if (!this._fileList) return;
- e.preventDefault();
+ ike.preventDefault();
this._navToFile(this._path, this._fileList, 1);
}
- _handleNextChunkOrCommentThread(e: CustomKeyboardEvent) {
- if (this.shouldSuppressKeyboardShortcut(e)) return;
+ _handleNextChunkOrCommentThread(e: IronKeyboardEvent) {
+ if (this.shortcuts.shouldSuppress(e)) return;
e.preventDefault();
if (e.detail.keyboardEvent?.shiftKey) {
@@ -729,8 +736,8 @@
this._navToFile(this._path, unreviewedFiles, direction === 'next' ? 1 : -1);
}
- _handlePrevChunkOrCommentThread(e: CustomKeyboardEvent) {
- if (this.shouldSuppressKeyboardShortcut(e)) return;
+ _handlePrevChunkOrCommentThread(e: IronKeyboardEvent) {
+ if (this.shortcuts.shouldSuppress(e)) return;
e.preventDefault();
if (e.detail.keyboardEvent?.shiftKey) {
@@ -745,8 +752,8 @@
}
// Similar to gr-change-view._handleOpenReplyDialog
- _handleOpenReplyDialog(e: CustomKeyboardEvent) {
- if (this.shouldSuppressKeyboardShortcut(e)) return;
+ _handleOpenReplyDialog(e: IronKeyboardEvent) {
+ if (this.shortcuts.shouldSuppress(e)) return;
if (this.modifierPressed(e)) return;
this._getLoggedIn().then(isLoggedIn => {
if (!isLoggedIn) {
@@ -760,16 +767,16 @@
});
}
- _handleToggleLeftPane(e: CustomKeyboardEvent) {
- if (this.shouldSuppressKeyboardShortcut(e)) return;
+ _handleToggleLeftPane(e: IronKeyboardEvent) {
+ if (this.shortcuts.shouldSuppress(e)) return;
if (!e.detail.keyboardEvent?.shiftKey) return;
e.preventDefault();
this.$.diffHost.toggleLeftDiff();
}
- _handleOpenDownloadDialog(e: CustomKeyboardEvent) {
- if (this.shouldSuppressKeyboardShortcut(e)) return;
+ _handleOpenDownloadDialog(e: IronKeyboardEvent) {
+ if (this.shortcuts.shouldSuppress(e)) return;
if (this.modifierPressed(e)) return;
this.set('changeViewState.showDownloadDialog', true);
@@ -777,16 +784,16 @@
this._navToChangeView();
}
- _handleUpToChange(e: CustomKeyboardEvent) {
- if (this.shouldSuppressKeyboardShortcut(e)) return;
+ _handleUpToChange(e: IronKeyboardEvent) {
+ if (this.shortcuts.shouldSuppress(e)) return;
if (this.modifierPressed(e)) return;
e.preventDefault();
this._navToChangeView();
}
- _handleCommaKey(e: CustomKeyboardEvent) {
- if (this.shouldSuppressKeyboardShortcut(e)) return;
+ _handleCommaKey(e: IronKeyboardEvent) {
+ if (this.shortcuts.shouldSuppress(e)) return;
if (this.modifierPressed(e)) return;
if (this._diffPrefsDisabled) return;
@@ -794,8 +801,8 @@
this.$.diffPreferencesDialog.open();
}
- _handleToggleDiffMode(e: CustomKeyboardEvent) {
- if (this.shouldSuppressKeyboardShortcut(e)) return;
+ _handleToggleDiffMode(e: IronKeyboardEvent) {
+ if (this.shortcuts.shouldSuppress(e)) return;
if (this.modifierPressed(e)) return;
e.preventDefault();
@@ -1691,28 +1698,28 @@
this._loadBlame();
}
- _handleToggleBlame(e: CustomKeyboardEvent) {
- if (this.shouldSuppressKeyboardShortcut(e)) return;
+ _handleToggleBlame(e: IronKeyboardEvent) {
+ if (this.shortcuts.shouldSuppress(e)) return;
if (this.modifierPressed(e)) return;
this._toggleBlame();
}
- _handleToggleHideAllCommentThreads(e: CustomKeyboardEvent) {
- if (this.shouldSuppressKeyboardShortcut(e)) return;
+ _handleToggleHideAllCommentThreads(e: IronKeyboardEvent) {
+ if (this.shortcuts.shouldSuppress(e)) return;
if (this.modifierPressed(e)) return;
toggleClass(this, 'hideComments');
}
- _handleOpenFileList(e: CustomKeyboardEvent) {
- if (this.shouldSuppressKeyboardShortcut(e)) return;
+ _handleOpenFileList(e: IronKeyboardEvent) {
+ if (this.shortcuts.shouldSuppress(e)) return;
if (this.modifierPressed(e)) return;
this.$.dropdown.open();
}
- _handleDiffAgainstBase(e: CustomKeyboardEvent) {
- if (this.shouldSuppressKeyboardShortcut(e)) return;
+ _handleDiffAgainstBase(e: IronKeyboardEvent) {
+ if (this.shortcuts.shouldSuppress(e)) return;
if (!this._change) return;
if (!this._path) return;
if (!this._patchRange) return;
@@ -1728,8 +1735,8 @@
);
}
- _handleDiffBaseAgainstLeft(e: CustomKeyboardEvent) {
- if (this.shouldSuppressKeyboardShortcut(e)) return;
+ _handleDiffBaseAgainstLeft(e: IronKeyboardEvent) {
+ if (this.shortcuts.shouldSuppress(e)) return;
if (!this._change) return;
if (!this._path) return;
if (!this._patchRange) return;
@@ -1749,8 +1756,8 @@
);
}
- _handleDiffAgainstLatest(e: CustomKeyboardEvent) {
- if (this.shouldSuppressKeyboardShortcut(e)) return;
+ _handleDiffAgainstLatest(e: IronKeyboardEvent) {
+ if (this.shortcuts.shouldSuppress(e)) return;
if (!this._change) return;
if (!this._path) return;
if (!this._patchRange) return;
@@ -1769,8 +1776,8 @@
);
}
- _handleDiffRightAgainstLatest(e: CustomKeyboardEvent) {
- if (this.shouldSuppressKeyboardShortcut(e)) return;
+ _handleDiffRightAgainstLatest(e: IronKeyboardEvent) {
+ if (this.shortcuts.shouldSuppress(e)) return;
if (!this._change) return;
if (!this._path) return;
if (!this._patchRange) return;
@@ -1788,8 +1795,8 @@
);
}
- _handleDiffBaseAgainstLatest(e: CustomKeyboardEvent) {
- if (this.shouldSuppressKeyboardShortcut(e)) return;
+ _handleDiffBaseAgainstLatest(e: IronKeyboardEvent) {
+ if (this.shortcuts.shouldSuppress(e)) return;
if (!this._change) return;
if (!this._path) return;
if (!this._patchRange) return;
@@ -1826,8 +1833,8 @@
return '';
}
- _handleToggleAllDiffContext(e: CustomKeyboardEvent) {
- if (this.shouldSuppressKeyboardShortcut(e)) return;
+ _handleToggleAllDiffContext(e: IronKeyboardEvent) {
+ if (this.shortcuts.shouldSuppress(e)) return;
this.$.diffHost.toggleAllContext();
}
@@ -1836,8 +1843,8 @@
return disableDiffPrefs || !loggedIn;
}
- _handleNextUnreviewedFile(e: CustomKeyboardEvent) {
- if (this.shouldSuppressKeyboardShortcut(e)) return;
+ _handleNextUnreviewedFile(e: IronKeyboardEvent) {
+ if (this.shortcuts.shouldSuppress(e)) return;
this._setReviewed(true);
this.navigateToUnreviewedFile('next');
}
@@ -1897,6 +1904,10 @@
_computeTruncatedPath(path?: string) {
return path ? computeTruncatedPath(path) : '';
}
+
+ createTitle(shortcutName: Shortcut, section: ShortcutSection) {
+ return this.shortcuts.createTitle(shortcutName, section);
+ }
}
declare global {
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view_html.ts b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view_html.ts
index 7e0ca10..b25be5a8 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view_html.ts
+++ b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view_html.ts
@@ -17,6 +17,9 @@
import {html} from '@polymer/polymer/lib/utils/html-tag';
export const htmlTemplate = html`
+ <style include="gr-a11y-styles">
+ /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
+ </style>
<style include="shared-styles">
:host {
display: block;
@@ -138,11 +141,6 @@
.separator.hide {
display: none;
}
- gr-dropdown-list {
- --trigger-style: {
- text-transform: none;
- }
- }
.editButtona a {
text-decoration: none;
}
@@ -351,15 +349,15 @@
hidden=""
>
<span class="preferences desktop">
- <gr-button
- link=""
- class="prefsButton"
+ <gr-tooltip-content
has-tooltip=""
position-below=""
title="Diff preferences"
- on-click="_handlePrefsTap"
- ><iron-icon icon="gr-icons:settings"></iron-icon
- ></gr-button>
+ >
+ <gr-button link="" class="prefsButton" on-click="_handlePrefsTap"
+ ><iron-icon icon="gr-icons:settings"></iron-icon
+ ></gr-button>
+ </gr-tooltip-content>
</span>
</span>
<gr-endpoint-decorator name="annotation-toggler">
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view_test.js b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view_test.js
index a3de30a..735624a 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view_test.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view_test.js
@@ -19,8 +19,7 @@
import './gr-diff-view.js';
import {GerritNav} from '../../core/gr-navigation/gr-navigation.js';
import {ChangeStatus} from '../../../constants/constants.js';
-import {TestKeyboardShortcutBinder, stubRestApi} from '../../../test/test-utils.js';
-import {Shortcut} from '../../../mixins/keyboard-shortcut-mixin/keyboard-shortcut-mixin.js';
+import {stubRestApi} from '../../../test/test-utils.js';
import {ChangeComments} from '../gr-comment-api/gr-comment-api.js';
import {GerritView} from '../../../services/router/router-model.js';
import {
@@ -41,42 +40,6 @@
let clock;
let diffCommentsStub;
- suiteSetup(() => {
- const kb = TestKeyboardShortcutBinder.push();
- kb.bindShortcut(Shortcut.LEFT_PANE, 'shift+left');
- kb.bindShortcut(Shortcut.RIGHT_PANE, 'shift+right');
- kb.bindShortcut(Shortcut.NEXT_LINE, 'j', 'down');
- kb.bindShortcut(Shortcut.PREV_LINE, 'k', 'up');
- kb.bindShortcut(Shortcut.NEXT_FILE_WITH_COMMENTS, 'shift+j');
- kb.bindShortcut(Shortcut.PREV_FILE_WITH_COMMENTS, 'shift+k');
- kb.bindShortcut(Shortcut.NEW_COMMENT, 'c');
- kb.bindShortcut(Shortcut.SAVE_COMMENT, 'ctrl+s');
- kb.bindShortcut(Shortcut.NEXT_FILE, ']');
- kb.bindShortcut(Shortcut.PREV_FILE, '[');
- kb.bindShortcut(Shortcut.NEXT_CHUNK, 'n');
- kb.bindShortcut(Shortcut.NEXT_COMMENT_THREAD, 'shift+n');
- kb.bindShortcut(Shortcut.PREV_CHUNK, 'p');
- kb.bindShortcut(Shortcut.PREV_COMMENT_THREAD, 'shift+p');
- kb.bindShortcut(Shortcut.OPEN_REPLY_DIALOG, 'a');
- kb.bindShortcut(Shortcut.OPEN_DOWNLOAD_DIALOG, 'd');
- kb.bindShortcut(Shortcut.TOGGLE_LEFT_PANE, 'shift+a');
- kb.bindShortcut(Shortcut.UP_TO_CHANGE, 'u');
- kb.bindShortcut(Shortcut.OPEN_DIFF_PREFS, ',');
- kb.bindShortcut(Shortcut.TOGGLE_DIFF_MODE, 'm');
- kb.bindShortcut(Shortcut.TOGGLE_FILE_REVIEWED, 'r');
- kb.bindShortcut(Shortcut.TOGGLE_ALL_DIFF_CONTEXT, 'shift+x');
- kb.bindShortcut(Shortcut.EXPAND_ALL_COMMENT_THREADS, 'e');
- kb.bindShortcut(Shortcut.TOGGLE_HIDE_ALL_COMMENT_THREADS, 'h');
- kb.bindShortcut(Shortcut.COLLAPSE_ALL_COMMENT_THREADS, 'shift+e');
- kb.bindShortcut(Shortcut.NEXT_UNREVIEWED_FILE, 'shift+m');
- kb.bindShortcut(Shortcut.TOGGLE_BLAME, 'b');
- kb.bindShortcut(Shortcut.OPEN_FILE_LIST, 'f');
- });
-
- suiteTeardown(() => {
- TestKeyboardShortcutBinder.pop();
- });
-
const PARENT = 'PARENT';
function getFilesFromFileList(fileList) {
@@ -504,16 +467,16 @@
sinon.stub(element, '_setReviewed');
sinon.spy(element, '_handleToggleFileReviewed');
element.$.reviewed.checked = false;
- MockInteractions.pressAndReleaseKeyOn(element, 82, 'shift', 'r');
+ MockInteractions.keyUpOn(element, 82, 'shift', 'r');
assert.isFalse(element._setReviewed.called);
assert.isTrue(element._handleToggleFileReviewed.calledOnce);
- MockInteractions.pressAndReleaseKeyOn(element, 82, null, 'r');
+ MockInteractions.keyUpOn(element, 82, null, 'r');
assert.isTrue(element._handleToggleFileReviewed.calledOnce);
clock.tick(1000);
- MockInteractions.pressAndReleaseKeyOn(element, 82, null, 'r');
+ MockInteractions.keyUpOn(element, 82, null, 'r');
assert.isTrue(element._handleToggleFileReviewed.calledTwice);
assert.isTrue(element._setReviewed.called);
assert.equal(element._setReviewed.lastCall.args[0], true);
@@ -573,7 +536,6 @@
basePatchNum: 5,
patchNum: 10,
};
- sinon.stub(element, 'shouldSuppressKeyboardShortcut').returns(false);
const diffNavStub = sinon.stub(GerritNav, 'navigateToDiff');
element._handleDiffAgainstBase(new CustomEvent(''));
const args = diffNavStub.getCall(0).args;
@@ -590,7 +552,6 @@
basePatchNum: 5,
patchNum: 10,
};
- sinon.stub(element, 'shouldSuppressKeyboardShortcut').returns(false);
const diffNavStub = sinon.stub(GerritNav, 'navigateToDiff');
element._handleDiffAgainstLatest(new CustomEvent(''));
const args = diffNavStub.getCall(0).args;
@@ -608,7 +569,6 @@
basePatchNum: 1,
};
element.params = {};
- sinon.stub(element, 'shouldSuppressKeyboardShortcut').returns(false);
const diffNavStub = sinon.stub(GerritNav, 'navigateToDiff');
element._handleDiffBaseAgainstLeft(new CustomEvent(''));
assert(diffNavStub.called);
@@ -631,7 +591,6 @@
sinon.stub(element, '_paramsChanged');
element.params = {commentLink: true, view: GerritView.DIFF};
element._focusLineNum = 10;
- sinon.stub(element, 'shouldSuppressKeyboardShortcut').returns(false);
const diffNavStub = sinon.stub(GerritNav, 'navigateToDiff');
element._handleDiffBaseAgainstLeft(new CustomEvent(''));
assert(diffNavStub.called);
@@ -650,7 +609,6 @@
basePatchNum: 1,
patchNum: 3,
};
- sinon.stub(element, 'shouldSuppressKeyboardShortcut').returns(false);
const diffNavStub = sinon.stub(GerritNav, 'navigateToDiff');
element._handleDiffRightAgainstLatest(new CustomEvent(''));
assert(diffNavStub.called);
@@ -668,7 +626,6 @@
basePatchNum: 1,
patchNum: 3,
};
- sinon.stub(element, 'shouldSuppressKeyboardShortcut').returns(false);
const diffNavStub = sinon.stub(GerritNav, 'navigateToDiff');
element._handleDiffBaseAgainstLatest(new CustomEvent(''));
assert(diffNavStub.called);
@@ -682,7 +639,7 @@
sinon.stub(element, '_getLoggedIn').returns(Promise.resolve(false));
const loggedInErrorSpy = sinon.spy();
element.addEventListener('show-auth-required', loggedInErrorSpy);
- MockInteractions.pressAndReleaseKeyOn(element, 65, null, 'a');
+ MockInteractions.keyUpOn(element, 65, null, 'a');
await flush();
assert.isTrue(changeNavStub.notCalled, 'The `a` keyboard shortcut ' +
'should only work when the user is logged in.');
@@ -708,7 +665,7 @@
sinon.stub(element, '_getLoggedIn').returns(Promise.resolve(true));
const loggedInErrorSpy = sinon.spy();
element.addEventListener('show-auth-required', loggedInErrorSpy);
- MockInteractions.pressAndReleaseKeyOn(element, 65, null, 'a');
+ MockInteractions.keyUpOn(element, 65, null, 'a');
await flush();
assert.isTrue(element.changeViewState.showReplyDialog);
assert(changeNavStub.lastCall.calledWithExactly(element._change, 10,
@@ -734,7 +691,7 @@
sinon.stub(element, '_getLoggedIn').returns(Promise.resolve(true));
const loggedInErrorSpy = sinon.spy();
element.addEventListener('show-auth-required', loggedInErrorSpy);
- MockInteractions.pressAndReleaseKeyOn(element, 65, null, 'a');
+ MockInteractions.keyUpOn(element, 65, null, 'a');
await flush();
assert.isTrue(element.changeViewState.showReplyDialog);
assert(changeNavStub.lastCall.calledWithExactly(element._change, 1,
@@ -798,7 +755,7 @@
'Should navigate to /c/42/5..10');
assert.isUndefined(element.changeViewState.showDownloadDialog);
- MockInteractions.pressAndReleaseKeyOn(element, 68, null, 'd');
+ MockInteractions.keyUpOn(element, 68, null, 'd');
assert.isTrue(element.changeViewState.showDownloadDialog);
});
@@ -1520,8 +1477,9 @@
});
test('_handleToggleDiffMode', () => {
- sinon.stub(element, 'shouldSuppressKeyboardShortcut').returns(false);
- const e = {preventDefault: () => {}};
+ const e = new CustomEvent('keydown', {
+ detail: {keyboardEvent: new KeyboardEvent('keydown'), key: 'x'},
+ });
// Initial state.
assert.equal(element._getDiffViewMode(), 'SIDE_BY_SIDE');
@@ -1732,7 +1690,7 @@
test('toggle blame with shortcut', () => {
const toggleBlame = sinon.stub(
element.$.diffHost, 'loadBlame').callsFake(() => Promise.resolve());
- MockInteractions.pressAndReleaseKeyOn(element, 66, null, 'b');
+ MockInteractions.keyUpOn(element, 66, null, 'b');
assert.isTrue(toggleBlame.calledOnce);
});
});
diff --git a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff-utils.ts b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff-utils.ts
index fada9cb..7393606 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff-utils.ts
+++ b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff-utils.ts
@@ -129,6 +129,29 @@
rootId: string;
}
+const VISIBLE_TEXT_NODE_TYPES = [Node.TEXT_NODE, Node.ELEMENT_NODE];
+
+export function getPreviousContentNodes(node?: Node | null) {
+ const sibs = [];
+ while (node) {
+ const {parentNode, previousSibling} = node;
+ const topContentLevel =
+ parentNode &&
+ (parentNode as HTMLElement).classList.contains('contentText');
+ let previousEl: Node | undefined | null;
+ if (previousSibling) {
+ previousEl = previousSibling;
+ } else if (!topContentLevel) {
+ previousEl = parentNode?.previousSibling;
+ }
+ if (previousEl && VISIBLE_TEXT_NODE_TYPES.includes(previousEl.nodeType)) {
+ sibs.push(previousEl);
+ }
+ node = previousEl;
+ }
+ return sibs;
+}
+
export function isThreadEl(node: Node): node is GrDiffThreadElement {
return (
node.nodeType === Node.ELEMENT_NODE &&
diff --git a/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select.ts b/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select.ts
index 3544834..0d6cadc 100644
--- a/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select.ts
+++ b/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select.ts
@@ -14,6 +14,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
+import '../../../styles/gr-a11y-styles';
import '../../../styles/shared-styles';
import '../../shared/gr-dropdown-list/gr-dropdown-list';
import '../../shared/gr-select/gr-select';
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 5ab8449..26944a4 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
@@ -17,6 +17,9 @@
import {html} from '@polymer/polymer/lib/utils/html-tag';
export const htmlTemplate = html`
+ <style include="gr-a11y-styles">
+ /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
+ </style>
<style include="shared-styles">
:host {
align-items: center;
@@ -30,11 +33,8 @@
margin: 0 var(--spacing-m);
}
gr-dropdown-list {
- --trigger-style: {
- color: var(--deemphasized-text-color);
- text-transform: none;
- font-family: var(--font-family);
- }
+ --trigger-style-text-color: var(--deemphasized-text-color);
+ --trigger-style-font-family: var(--font-family);
}
@media screen and (max-width: 50em) {
.filesWeblinks {
diff --git a/polygerrit-ui/app/elements/diff/gr-range-header/gr-range-header.ts b/polygerrit-ui/app/elements/diff/gr-range-header/gr-range-header.ts
index 1c2c074..dcf7236 100644
--- a/polygerrit-ui/app/elements/diff/gr-range-header/gr-range-header.ts
+++ b/polygerrit-ui/app/elements/diff/gr-range-header/gr-range-header.ts
@@ -16,6 +16,7 @@
*/
import {LitElement, css, html} from 'lit';
import {customElement, property} from 'lit/decorators';
+import '@polymer/iron-icon/iron-icon';
/**
* Represents a header (label) for a code chunk whenever showing
diff --git a/polygerrit-ui/app/elements/diff/gr-ranged-comment-hint/gr-ranged-comment-hint.ts b/polygerrit-ui/app/elements/diff/gr-ranged-comment-hint/gr-ranged-comment-hint.ts
index bc3c054..3f2258d 100644
--- a/polygerrit-ui/app/elements/diff/gr-ranged-comment-hint/gr-ranged-comment-hint.ts
+++ b/polygerrit-ui/app/elements/diff/gr-ranged-comment-hint/gr-ranged-comment-hint.ts
@@ -45,6 +45,7 @@
override render() {
// To pass CSS mixins for @apply to Polymer components, they need to appear
// in <style> inside the template.
+ /* eslint-disable lit/prefer-static-styles */
const customStyle = html`
<style>
.row {
diff --git a/polygerrit-ui/app/elements/diff/gr-selection-action-box/gr-selection-action-box.ts b/polygerrit-ui/app/elements/diff/gr-selection-action-box/gr-selection-action-box.ts
index 3b026b3..8821725 100644
--- a/polygerrit-ui/app/elements/diff/gr-selection-action-box/gr-selection-action-box.ts
+++ b/polygerrit-ui/app/elements/diff/gr-selection-action-box/gr-selection-action-box.ts
@@ -15,6 +15,7 @@
* limitations under the License.
*/
import '../../../styles/shared-styles';
+import '../../shared/gr-tooltip/gr-tooltip';
import {GrTooltip} from '../../shared/gr-tooltip/gr-tooltip';
import {customElement, property} from '@polymer/decorators';
import {flush} from '@polymer/polymer/lib/legacy/polymer.dom';
diff --git a/polygerrit-ui/app/elements/documentation/gr-documentation-search/gr-documentation-search.ts b/polygerrit-ui/app/elements/documentation/gr-documentation-search/gr-documentation-search.ts
index 91882ab..3adb0f3 100644
--- a/polygerrit-ui/app/elements/documentation/gr-documentation-search/gr-documentation-search.ts
+++ b/polygerrit-ui/app/elements/documentation/gr-documentation-search/gr-documentation-search.ts
@@ -19,21 +19,15 @@
import '../../shared/gr-list-view/gr-list-view';
import {PolymerElement} from '@polymer/polymer/polymer-element';
import {htmlTemplate} from './gr-documentation-search_html';
-import {
- ListViewMixin,
- ListViewParams,
-} from '../../../mixins/gr-list-view-mixin/gr-list-view-mixin';
import {getBaseUrl} from '../../../utils/url-util';
import {customElement, property} from '@polymer/decorators';
import {DocResult} from '../../../types/common';
import {fireTitleChange} from '../../../utils/event-util';
import {appContext} from '../../../services/app-context';
-
-// This avoids JSC_DYNAMIC_EXTENDS_WITHOUT_JSDOC closure compiler error.
-const base = ListViewMixin(PolymerElement);
+import {ListViewParams} from '../../gr-app-types';
@customElement('gr-documentation-search')
-export class GrDocumentationSearch extends base {
+export class GrDocumentationSearch extends PolymerElement {
static get template() {
return htmlTemplate;
}
@@ -62,7 +56,7 @@
_paramsChanged(params: ListViewParams) {
this._loading = true;
- this._filter = this.getFilterValue(params);
+ this._filter = params?.filter ?? '';
return this._getDocumentationSearches(this._filter);
}
@@ -87,6 +81,10 @@
}
return `${getBaseUrl()}/${url}`;
}
+
+ computeLoadingClass(loading: boolean) {
+ return loading ? 'loading' : '';
+ }
}
declare global {
diff --git a/polygerrit-ui/app/elements/documentation/gr-documentation-search/gr-documentation-search_test.ts b/polygerrit-ui/app/elements/documentation/gr-documentation-search/gr-documentation-search_test.ts
index 5267b2d..bf6a0d5 100644
--- a/polygerrit-ui/app/elements/documentation/gr-documentation-search/gr-documentation-search_test.ts
+++ b/polygerrit-ui/app/elements/documentation/gr-documentation-search/gr-documentation-search_test.ts
@@ -21,8 +21,8 @@
import {page} from '../../../utils/page-wrapper-utils';
import 'lodash/lodash';
import {stubRestApi} from '../../../test/test-utils';
-import {ListViewParams} from '../../../mixins/gr-list-view-mixin/gr-list-view-mixin';
import {DocResult} from '../../../types/common';
+import {ListViewParams} from '../../gr-app-types';
const basicFixture = fixtureFromElement('gr-documentation-search');
diff --git a/polygerrit-ui/app/elements/edit/gr-default-editor/gr-default-editor.ts b/polygerrit-ui/app/elements/edit/gr-default-editor/gr-default-editor.ts
index 480b8fe..5312be2 100644
--- a/polygerrit-ui/app/elements/edit/gr-default-editor/gr-default-editor.ts
+++ b/polygerrit-ui/app/elements/edit/gr-default-editor/gr-default-editor.ts
@@ -14,7 +14,6 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-
import {sharedStyles} from '../../../styles/shared-styles';
import {LitElement, css, html} from 'lit';
import {customElement, property} from 'lit/decorators';
@@ -62,7 +61,7 @@
override render() {
return html` <textarea
id="textarea"
- value="${this.fileContent}"
+ .value="${this.fileContent}"
@input=${this._handleTextareaInput}
></textarea>`;
}
diff --git a/polygerrit-ui/app/elements/edit/gr-edit-controls/gr-edit-controls.ts b/polygerrit-ui/app/elements/edit/gr-edit-controls/gr-edit-controls.ts
index f0a3ca1..1615a23 100644
--- a/polygerrit-ui/app/elements/edit/gr-edit-controls/gr-edit-controls.ts
+++ b/polygerrit-ui/app/elements/edit/gr-edit-controls/gr-edit-controls.ts
@@ -36,7 +36,7 @@
} from '../../shared/gr-autocomplete/gr-autocomplete';
import {appContext} from '../../../services/app-context';
import {IronInputElement} from '@polymer/iron-input';
-import {fireAlert} from '../../../utils/event-util';
+import {fireAlert, fireReload} from '../../../utils/event-util';
export interface GrEditControls {
$: {
@@ -237,7 +237,7 @@
return;
}
this._closeDialog(this.$.openDialog);
- GerritNav.navigateToChange(this.change);
+ fireReload(this, true);
});
}
@@ -257,7 +257,7 @@
return;
}
this._closeDialog(dialog);
- GerritNav.navigateToChange(this.change);
+ fireReload(this);
});
}
@@ -275,7 +275,7 @@
return;
}
this._closeDialog(dialog);
- GerritNav.navigateToChange(this.change);
+ fireReload(this);
});
}
@@ -293,7 +293,7 @@
return;
}
this._closeDialog(dialog);
- GerritNav.navigateToChange(this.change);
+ fireReload(this, true);
});
}
@@ -351,7 +351,7 @@
}
}
- _handleKeyPress(event: InputEvent) {
+ _handleKeyPress(event: KeyboardEvent) {
event.preventDefault();
event.stopImmediatePropagation();
}
diff --git a/polygerrit-ui/app/elements/edit/gr-edit-controls/gr-edit-controls_test.ts b/polygerrit-ui/app/elements/edit/gr-edit-controls/gr-edit-controls_test.ts
index 2e1fc21..6198f17 100644
--- a/polygerrit-ui/app/elements/edit/gr-edit-controls/gr-edit-controls_test.ts
+++ b/polygerrit-ui/app/elements/edit/gr-edit-controls/gr-edit-controls_test.ts
@@ -76,33 +76,33 @@
assert.isTrue(element._isValidPath('test.js'));
});
- test('open', () => {
+ test('open', async () => {
assert.isFalse(hideDialogStub.called);
MockInteractions.tap(queryAndAssert(element, '#open'));
element.patchNum = 1 as PatchSetNum;
- return showDialogSpy.lastCall.returnValue.then(() => {
- assert.isTrue(hideDialogStub.called);
- assert.isTrue(element.$.openDialog.disabled);
- assert.isFalse(queryStub.called);
- // Setup _focused manually - in headless mode Chrome sometimes don't
- // setup focus. flush and/or flushAsynchronousOperations don't help
- openAutoComplete._focused = true;
- openAutoComplete.noDebounce = true;
- openAutoComplete.text = 'src/test.cpp';
- assert.isTrue(queryStub.called);
- assert.isFalse(element.$.openDialog.disabled);
- MockInteractions.tap(
- queryAndAssert(element.$.openDialog, 'gr-button[primary]')
- );
- assert.isTrue(editDiffStub.called);
- assert.isTrue(navStub.called);
- assert.deepEqual(editDiffStub.lastCall.args, [
- element.change,
- 'src/test.cpp',
- element.patchNum,
- ]);
- assert.isTrue(closeDialogSpy.called);
- });
+ await showDialogSpy.lastCall.returnValue;
+ assert.isTrue(hideDialogStub.called);
+ assert.isTrue(element.$.openDialog.disabled);
+ assert.isFalse(queryStub.called);
+ // Setup _focused manually - in headless mode Chrome sometimes don't
+ // setup focus. flush and/or flushAsynchronousOperations don't help
+ openAutoComplete._focused = true;
+ openAutoComplete.noDebounce = true;
+ openAutoComplete.text = 'src/test.cpp';
+ await flush();
+ assert.isTrue(queryStub.called);
+ assert.isFalse(element.$.openDialog.disabled);
+ MockInteractions.tap(
+ queryAndAssert(element.$.openDialog, 'gr-button[primary]')
+ );
+ assert.isTrue(editDiffStub.called);
+ assert.isTrue(navStub.called);
+ assert.deepEqual(editDiffStub.lastCall.args, [
+ element.change,
+ 'src/test.cpp',
+ element.patchNum,
+ ]);
+ assert.isTrue(closeDialogSpy.called);
});
test('cancel', () => {
@@ -122,70 +122,67 @@
});
suite('delete button CUJ', () => {
- let navStub: sinon.SinonStub;
+ let eventStub: sinon.SinonStub;
let deleteStub: sinon.SinonStub;
let deleteAutocomplete: GrAutocomplete;
setup(() => {
- navStub = sinon.stub(GerritNav, 'navigateToChange');
+ eventStub = sinon.stub(element, 'dispatchEvent');
deleteStub = stubRestApi('deleteFileInChangeEdit');
deleteAutocomplete =
element.$.deleteDialog!.querySelector('gr-autocomplete')!;
});
- test('delete', () => {
+ test('delete', async () => {
deleteStub.returns(Promise.resolve({ok: true}));
MockInteractions.tap(queryAndAssert(element, '#delete'));
- return showDialogSpy.lastCall.returnValue.then(() => {
- assert.isTrue(element.$.deleteDialog.disabled);
- assert.isFalse(queryStub.called);
- // Setup _focused manually - in headless mode Chrome sometimes don't
- // setup focus. flush and/or flushAsynchronousOperations don't help
- deleteAutocomplete._focused = true;
- deleteAutocomplete.noDebounce = true;
- deleteAutocomplete.text = 'src/test.cpp';
- assert.isTrue(queryStub.called);
- assert.isFalse(element.$.deleteDialog.disabled);
- MockInteractions.tap(
- queryAndAssert(element.$.deleteDialog, 'gr-button[primary]')
- );
- flush();
+ await showDialogSpy.lastCall.returnValue;
+ assert.isTrue(element.$.deleteDialog.disabled);
+ assert.isFalse(queryStub.called);
+ // Setup _focused manually - in headless mode Chrome sometimes don't
+ // setup focus. flush and/or flushAsynchronousOperations don't help
+ deleteAutocomplete._focused = true;
+ deleteAutocomplete.noDebounce = true;
+ deleteAutocomplete.text = 'src/test.cpp';
+ await flush();
+ assert.isTrue(queryStub.called);
+ assert.isFalse(element.$.deleteDialog.disabled);
+ MockInteractions.tap(
+ queryAndAssert(element.$.deleteDialog, 'gr-button[primary]')
+ );
+ await flush();
- assert.isTrue(deleteStub.called);
-
- return deleteStub.lastCall.returnValue.then(() => {
- assert.equal(element._path, '');
- assert.isTrue(navStub.called);
- assert.isTrue(closeDialogSpy.called);
- });
- });
+ assert.isTrue(deleteStub.called);
+ await deleteStub.lastCall.returnValue;
+ assert.equal(element._path, '');
+ assert.equal(eventStub.firstCall.args[0].type, 'reload');
+ assert.isTrue(closeDialogSpy.called);
});
- test('delete fails', () => {
+ test('delete fails', async () => {
deleteStub.returns(Promise.resolve({ok: false}));
MockInteractions.tap(queryAndAssert(element, '#delete'));
- return showDialogSpy.lastCall.returnValue.then(() => {
- assert.isTrue(element.$.deleteDialog.disabled);
- assert.isFalse(queryStub.called);
- // Setup _focused manually - in headless mode Chrome sometimes don't
- // setup focus. flush and/or flushAsynchronousOperations don't help
- deleteAutocomplete._focused = true;
- deleteAutocomplete.noDebounce = true;
- deleteAutocomplete.text = 'src/test.cpp';
- assert.isTrue(queryStub.called);
- assert.isFalse(element.$.deleteDialog.disabled);
- MockInteractions.tap(
- queryAndAssert(element.$.deleteDialog, 'gr-button[primary]')
- );
- flush();
+ await showDialogSpy.lastCall.returnValue;
+ assert.isTrue(element.$.deleteDialog.disabled);
+ assert.isFalse(queryStub.called);
+ // Setup _focused manually - in headless mode Chrome sometimes don't
+ // setup focus. flush and/or flushAsynchronousOperations don't help
+ deleteAutocomplete._focused = true;
+ deleteAutocomplete.noDebounce = true;
+ deleteAutocomplete.text = 'src/test.cpp';
+ await flush();
+ assert.isTrue(queryStub.called);
+ assert.isFalse(element.$.deleteDialog.disabled);
+ MockInteractions.tap(
+ queryAndAssert(element.$.deleteDialog, 'gr-button[primary]')
+ );
+ await flush();
- assert.isTrue(deleteStub.called);
+ assert.isTrue(deleteStub.called);
- return deleteStub.lastCall.returnValue.then(() => {
- assert.isFalse(navStub.called);
- assert.isFalse(closeDialogSpy.called);
- });
- });
+ await deleteStub.lastCall.returnValue;
+ assert.isFalse(eventStub.called);
+ assert.isFalse(closeDialogSpy.called);
});
test('cancel', () => {
@@ -198,7 +195,7 @@
MockInteractions.tap(
queryAndAssert(element.$.deleteDialog, 'gr-button')
);
- assert.isFalse(navStub.called);
+ assert.isFalse(eventStub.called);
assert.isTrue(closeDialogSpy.called);
assert.equal(element._path, '');
});
@@ -206,78 +203,77 @@
});
suite('rename button CUJ', () => {
- let navStub: sinon.SinonStub;
+ let eventStub: sinon.SinonStub;
let renameStub: sinon.SinonStub;
let renameAutocomplete: GrAutocomplete;
setup(() => {
- navStub = sinon.stub(GerritNav, 'navigateToChange');
+ eventStub = sinon.stub(element, 'dispatchEvent');
renameStub = stubRestApi('renameFileInChangeEdit');
renameAutocomplete =
element.$.renameDialog!.querySelector('gr-autocomplete')!;
});
- test('rename', () => {
+ test('rename', async () => {
renameStub.returns(Promise.resolve({ok: true}));
MockInteractions.tap(queryAndAssert(element, '#rename'));
- return showDialogSpy.lastCall.returnValue.then(() => {
- assert.isTrue(element.$.renameDialog.disabled);
- assert.isFalse(queryStub.called);
- // Setup _focused manually - in headless mode Chrome sometimes don't
- // setup focus. flush and/or flushAsynchronousOperations don't help
- renameAutocomplete._focused = true;
- renameAutocomplete.noDebounce = true;
- renameAutocomplete.text = 'src/test.cpp';
- assert.isTrue(queryStub.called);
- assert.isTrue(element.$.renameDialog.disabled);
+ await showDialogSpy.lastCall.returnValue;
+ assert.isTrue(element.$.renameDialog.disabled);
+ assert.isFalse(queryStub.called);
+ // Setup _focused manually - in headless mode Chrome sometimes don't
+ // setup focus. flush and/or flushAsynchronousOperations don't help
+ renameAutocomplete._focused = true;
+ renameAutocomplete.noDebounce = true;
+ renameAutocomplete.text = 'src/test.cpp';
+ await flush();
+ assert.isTrue(queryStub.called);
+ assert.isTrue(element.$.renameDialog.disabled);
- element.$.newPathIronInput.bindValue = 'src/test.newPath';
+ element.$.newPathIronInput.bindValue = 'src/test.newPath';
+ await flush();
- assert.isFalse(element.$.renameDialog.disabled);
- MockInteractions.tap(
- queryAndAssert(element.$.renameDialog, 'gr-button[primary]')
- );
- flush();
+ assert.isFalse(element.$.renameDialog.disabled);
+ MockInteractions.tap(
+ queryAndAssert(element.$.renameDialog, 'gr-button[primary]')
+ );
+ await flush();
+ assert.isTrue(renameStub.called);
- assert.isTrue(renameStub.called);
-
- return renameStub.lastCall.returnValue.then(() => {
- assert.equal(element._path, '');
- assert.isTrue(navStub.called);
- assert.isTrue(closeDialogSpy.called);
- });
- });
+ await renameStub.lastCall.returnValue;
+ assert.equal(element._path, '');
+ assert.equal(eventStub.firstCall.args[0].type, 'reload');
+ assert.isTrue(closeDialogSpy.called);
});
- test('rename fails', () => {
+ test('rename fails', async () => {
renameStub.returns(Promise.resolve({ok: false}));
MockInteractions.tap(queryAndAssert(element, '#rename'));
- return showDialogSpy.lastCall.returnValue.then(() => {
- assert.isTrue(element.$.renameDialog.disabled);
- assert.isFalse(queryStub.called);
- // Setup _focused manually - in headless mode Chrome sometimes don't
- // setup focus. flush and/or flushAsynchronousOperations don't help
- renameAutocomplete._focused = true;
- renameAutocomplete.noDebounce = true;
- renameAutocomplete.text = 'src/test.cpp';
- assert.isTrue(queryStub.called);
- assert.isTrue(element.$.renameDialog.disabled);
+ await showDialogSpy.lastCall.returnValue;
+ assert.isTrue(element.$.renameDialog.disabled);
+ assert.isFalse(queryStub.called);
+ // Setup _focused manually - in headless mode Chrome sometimes don't
+ // setup focus. flush and/or flushAsynchronousOperations don't help
+ renameAutocomplete._focused = true;
+ renameAutocomplete.noDebounce = true;
+ renameAutocomplete.text = 'src/test.cpp';
+ await flush();
+ assert.isTrue(queryStub.called);
+ assert.isTrue(element.$.renameDialog.disabled);
- element.$.newPathIronInput.bindValue = 'src/test.newPath';
+ element.$.newPathIronInput.bindValue = 'src/test.newPath';
+ await flush();
- assert.isFalse(element.$.renameDialog.disabled);
- MockInteractions.tap(
- queryAndAssert(element.$.renameDialog, 'gr-button[primary]')
- );
- flush();
+ assert.isFalse(element.$.renameDialog.disabled);
+ MockInteractions.tap(
+ queryAndAssert(element.$.renameDialog, 'gr-button[primary]')
+ );
+ await flush();
- assert.isTrue(renameStub.called);
+ assert.isTrue(renameStub.called);
- return renameStub.lastCall.returnValue.then(() => {
- assert.isFalse(navStub.called);
- assert.isFalse(closeDialogSpy.called);
- });
- });
+ await renameStub.lastCall.returnValue;
+ assert.isFalse(eventStub.called);
+ assert.isFalse(closeDialogSpy.called);
});
test('cancel', () => {
@@ -291,7 +287,7 @@
MockInteractions.tap(
queryAndAssert(element.$.renameDialog, 'gr-button')
);
- assert.isFalse(navStub.called);
+ assert.isFalse(eventStub.called);
assert.isTrue(closeDialogSpy.called);
assert.equal(element._path, '');
assert.equal(element._newPath, '');
@@ -300,11 +296,11 @@
});
suite('restore button CUJ', () => {
- let navStub: sinon.SinonStub;
+ let eventStub: sinon.SinonStub;
let restoreStub: sinon.SinonStub;
setup(() => {
- navStub = sinon.stub(GerritNav, 'navigateToChange');
+ eventStub = sinon.stub(element, 'dispatchEvent');
restoreStub = stubRestApi('restoreFileInChangeEdit');
});
@@ -328,7 +324,7 @@
assert.equal(restoreStub.lastCall.args[1], 'src/test.cpp');
return restoreStub.lastCall.returnValue.then(() => {
assert.equal(element._path, '');
- assert.isTrue(navStub.called);
+ assert.equal(eventStub.firstCall.args[0].type, 'reload');
assert.isTrue(closeDialogSpy.called);
});
});
@@ -347,7 +343,7 @@
assert.isTrue(restoreStub.called);
assert.equal(restoreStub.lastCall.args[1], 'src/test.cpp');
return restoreStub.lastCall.returnValue.then(() => {
- assert.isFalse(navStub.called);
+ assert.isFalse(eventStub.called);
assert.isFalse(closeDialogSpy.called);
});
});
@@ -360,7 +356,7 @@
MockInteractions.tap(
queryAndAssert(element.$.restoreDialog, 'gr-button')
);
- assert.isFalse(navStub.called);
+ assert.isFalse(eventStub.called);
assert.isTrue(closeDialogSpy.called);
assert.equal(element._path, '');
});
diff --git a/polygerrit-ui/app/elements/edit/gr-edit-file-controls/gr-edit-file-controls.ts b/polygerrit-ui/app/elements/edit/gr-edit-file-controls/gr-edit-file-controls.ts
index 1255256..418c368 100644
--- a/polygerrit-ui/app/elements/edit/gr-edit-file-controls/gr-edit-file-controls.ts
+++ b/polygerrit-ui/app/elements/edit/gr-edit-file-controls/gr-edit-file-controls.ts
@@ -49,6 +49,9 @@
display: flex;
justify-content: flex-end;
}
+ gr-dropdown {
+ --gr-button-padding: var(--spacing-xs) var(--spacing-s);
+ }
#actions {
margin-right: var(--spacing-l);
}
@@ -59,14 +62,9 @@
override render() {
// To pass CSS mixins for @apply to Polymer components, they need to appear
// in <style> inside the template.
+ /* eslint-disable lit/prefer-static-styles */
const customStyle = html`
<style>
- gr-button,
- gr-dropdown {
- --gr-button: {
- height: 1.8em;
- }
- }
gr-dropdown {
--gr-dropdown-item: {
background-color: transparent;
diff --git a/polygerrit-ui/app/elements/edit/gr-editor-view/gr-editor-view.ts b/polygerrit-ui/app/elements/edit/gr-editor-view/gr-editor-view.ts
index 6f2d27e..ad7e015 100644
--- a/polygerrit-ui/app/elements/edit/gr-editor-view/gr-editor-view.ts
+++ b/polygerrit-ui/app/elements/edit/gr-editor-view/gr-editor-view.ts
@@ -47,6 +47,7 @@
import {GrButton} from '../../shared/gr-button/gr-button';
import {GrDefaultEditor} from '../gr-default-editor/gr-default-editor';
import {GrEndpointDecorator} from '../../plugins/gr-endpoint-decorator/gr-endpoint-decorator';
+import {IronKeyboardEvent} from '../../../types/events';
const RESTORED_MESSAGE = 'Content restored from a previous edit.';
const SAVING_MESSAGE = 'Saving changes...';
@@ -393,7 +394,7 @@
);
}
- _handleSaveShortcut(e: KeyboardEvent) {
+ _handleSaveShortcut(e: IronKeyboardEvent) {
e.preventDefault();
if (!this._saveDisabled) {
this._saveEdit();
diff --git a/polygerrit-ui/app/elements/gr-app-element.ts b/polygerrit-ui/app/elements/gr-app-element.ts
index 46a3e84..38f55bd 100644
--- a/polygerrit-ui/app/elements/gr-app-element.ts
+++ b/polygerrit-ui/app/elements/gr-app-element.ts
@@ -43,7 +43,6 @@
import {
KeyboardShortcutMixin,
Shortcut,
- SPECIAL_SHORTCUT,
} from '../mixins/keyboard-shortcut-mixin/keyboard-shortcut-mixin';
import {GerritNav} from './core/gr-navigation/gr-navigation';
import {appContext} from '../services/app-context';
@@ -69,7 +68,7 @@
import {GrMainHeader} from './core/gr-main-header/gr-main-header';
import {GrSettingsView} from './settings/gr-settings-view/gr-settings-view';
import {
- CustomKeyboardEvent,
+ IronKeyboardEvent,
DialogChangeEventDetail,
EventType,
LocationChangeEvent,
@@ -215,6 +214,8 @@
private readonly restApiService = appContext.restApiService;
+ private readonly shortcuts = appContext.shortcutsService;
+
override keyboardShortcuts() {
return {
[Shortcut.OPEN_SHORTCUT_HELP_DIALOG]: '_showKeyboardShortcuts',
@@ -232,7 +233,6 @@
// model changes and updates the config model, but at the moment the service
// is not called from anywhere.
appContext.configService;
- this._bindKeyboardShortcuts();
document.addEventListener(EventType.PAGE_ERROR, e => {
this._handlePageError(e);
});
@@ -242,7 +242,7 @@
this.addEventListener(EventType.DIALOG_CHANGE, e => {
this._handleDialogChange(e as CustomEvent<DialogChangeEventDetail>);
});
- this.addEventListener('location-change', e =>
+ this.addEventListener(EventType.LOCATION_CHANGE, e =>
this._handleLocationChange(e)
);
this.addEventListener(EventType.RECREATE_CHANGE_VIEW, () =>
@@ -251,7 +251,7 @@
this.addEventListener(EventType.RECREATE_DIFF_VIEW, () =>
this.handleRecreateView(GerritView.DIFF)
);
- document.addEventListener('gr-rpc-log', e => this._handleRpcLog(e));
+ document.addEventListener(EventType.GR_RPC_LOG, e => this._handleRpcLog(e));
}
override ready() {
@@ -307,158 +307,6 @@
};
}
- _bindKeyboardShortcuts() {
- this.bindShortcut(
- Shortcut.SEND_REPLY,
- SPECIAL_SHORTCUT.DOC_ONLY,
- 'ctrl+enter',
- 'meta+enter'
- );
- this.bindShortcut(Shortcut.EMOJI_DROPDOWN, SPECIAL_SHORTCUT.DOC_ONLY, ':');
-
- this.bindShortcut(Shortcut.OPEN_SHORTCUT_HELP_DIALOG, '?');
- this.bindShortcut(
- Shortcut.GO_TO_USER_DASHBOARD,
- SPECIAL_SHORTCUT.GO_KEY,
- 'i'
- );
- this.bindShortcut(
- Shortcut.GO_TO_OPENED_CHANGES,
- SPECIAL_SHORTCUT.GO_KEY,
- 'o'
- );
- this.bindShortcut(
- Shortcut.GO_TO_MERGED_CHANGES,
- SPECIAL_SHORTCUT.GO_KEY,
- 'm'
- );
- this.bindShortcut(
- Shortcut.GO_TO_ABANDONED_CHANGES,
- SPECIAL_SHORTCUT.GO_KEY,
- 'a'
- );
- this.bindShortcut(
- Shortcut.GO_TO_WATCHED_CHANGES,
- SPECIAL_SHORTCUT.GO_KEY,
- 'w'
- );
-
- this.bindShortcut(Shortcut.CURSOR_NEXT_CHANGE, 'j');
- this.bindShortcut(Shortcut.CURSOR_PREV_CHANGE, 'k');
- this.bindShortcut(Shortcut.OPEN_CHANGE, 'o');
- this.bindShortcut(Shortcut.NEXT_PAGE, 'n', ']');
- this.bindShortcut(Shortcut.PREV_PAGE, 'p', '[');
- this.bindShortcut(Shortcut.TOGGLE_CHANGE_REVIEWED, 'r:keyup');
- this.bindShortcut(Shortcut.TOGGLE_CHANGE_STAR, 's:keydown');
- this.bindShortcut(Shortcut.REFRESH_CHANGE_LIST, 'shift+r:keyup');
- this.bindShortcut(Shortcut.EDIT_TOPIC, 't');
- this.bindShortcut(Shortcut.OPEN_SUBMIT_DIALOG, 'shift+s');
-
- this.bindShortcut(Shortcut.OPEN_REPLY_DIALOG, 'a:keyup');
- this.bindShortcut(Shortcut.OPEN_DOWNLOAD_DIALOG, 'd:keyup');
- this.bindShortcut(Shortcut.EXPAND_ALL_MESSAGES, 'x');
- this.bindShortcut(Shortcut.COLLAPSE_ALL_MESSAGES, 'z');
- this.bindShortcut(Shortcut.REFRESH_CHANGE, 'shift+r:keyup');
- this.bindShortcut(Shortcut.UP_TO_DASHBOARD, 'u');
- this.bindShortcut(Shortcut.UP_TO_CHANGE, 'u');
- this.bindShortcut(Shortcut.TOGGLE_DIFF_MODE, 'm:keyup');
- this.bindShortcut(
- Shortcut.DIFF_AGAINST_BASE,
- SPECIAL_SHORTCUT.V_KEY,
- 'down',
- 's'
- );
- // this keyboard shortcut is used in toast _displayDiffAgainstLatestToast
- // in gr-diff-view. Any updates here should be reflected there
- this.bindShortcut(
- Shortcut.DIFF_AGAINST_LATEST,
- SPECIAL_SHORTCUT.V_KEY,
- 'up',
- 'w'
- );
- // this keyboard shortcut is used in toast _displayDiffBaseAgainstLeftToast
- // in gr-diff-view. Any updates here should be reflected there
- this.bindShortcut(
- Shortcut.DIFF_BASE_AGAINST_LEFT,
- SPECIAL_SHORTCUT.V_KEY,
- 'left',
- 'a'
- );
- this.bindShortcut(
- Shortcut.DIFF_RIGHT_AGAINST_LATEST,
- SPECIAL_SHORTCUT.V_KEY,
- 'right',
- 'd'
- );
- this.bindShortcut(
- Shortcut.DIFF_BASE_AGAINST_LATEST,
- SPECIAL_SHORTCUT.V_KEY,
- 'b'
- );
-
- this.bindShortcut(Shortcut.NEXT_LINE, 'j', 'down');
- this.bindShortcut(Shortcut.PREV_LINE, 'k', 'up');
- if (this._isCursorManagerSupportMoveToVisibleLine()) {
- this.bindShortcut(Shortcut.VISIBLE_LINE, '.');
- }
- this.bindShortcut(Shortcut.NEXT_CHUNK, 'n');
- this.bindShortcut(Shortcut.PREV_CHUNK, 'p');
- this.bindShortcut(Shortcut.TOGGLE_ALL_DIFF_CONTEXT, 'shift+x');
- this.bindShortcut(Shortcut.NEXT_COMMENT_THREAD, 'shift+n');
- this.bindShortcut(Shortcut.PREV_COMMENT_THREAD, 'shift+p');
- this.bindShortcut(
- Shortcut.EXPAND_ALL_COMMENT_THREADS,
- SPECIAL_SHORTCUT.DOC_ONLY,
- 'e'
- );
- this.bindShortcut(
- Shortcut.COLLAPSE_ALL_COMMENT_THREADS,
- SPECIAL_SHORTCUT.DOC_ONLY,
- 'shift+e'
- );
- this.bindShortcut(Shortcut.LEFT_PANE, 'shift+left');
- this.bindShortcut(Shortcut.RIGHT_PANE, 'shift+right');
- this.bindShortcut(Shortcut.TOGGLE_LEFT_PANE, 'shift+a');
- this.bindShortcut(Shortcut.NEW_COMMENT, 'c');
- this.bindShortcut(
- Shortcut.SAVE_COMMENT,
- 'ctrl+enter',
- 'meta+enter',
- 'ctrl+s',
- 'meta+s'
- );
- this.bindShortcut(Shortcut.OPEN_DIFF_PREFS, ',');
- this.bindShortcut(Shortcut.TOGGLE_DIFF_REVIEWED, 'r:keyup');
-
- this.bindShortcut(Shortcut.NEXT_FILE, ']');
- this.bindShortcut(Shortcut.PREV_FILE, '[');
- this.bindShortcut(Shortcut.NEXT_FILE_WITH_COMMENTS, 'shift+j');
- this.bindShortcut(Shortcut.PREV_FILE_WITH_COMMENTS, 'shift+k');
- this.bindShortcut(Shortcut.CURSOR_NEXT_FILE, 'j', 'down');
- this.bindShortcut(Shortcut.CURSOR_PREV_FILE, 'k', 'up');
- this.bindShortcut(Shortcut.OPEN_FILE, 'o', 'enter');
- this.bindShortcut(Shortcut.TOGGLE_FILE_REVIEWED, 'r:keyup');
- this.bindShortcut(Shortcut.NEXT_UNREVIEWED_FILE, 'shift+m');
- this.bindShortcut(Shortcut.TOGGLE_ALL_INLINE_DIFFS, 'shift+i');
- this.bindShortcut(Shortcut.TOGGLE_INLINE_DIFF, 'i');
- this.bindShortcut(Shortcut.TOGGLE_BLAME, 'b:keyup');
- this.bindShortcut(Shortcut.TOGGLE_HIDE_ALL_COMMENT_THREADS, 'h');
- this.bindShortcut(Shortcut.OPEN_FILE_LIST, 'f');
-
- this.bindShortcut(Shortcut.OPEN_FIRST_FILE, ']');
- this.bindShortcut(Shortcut.OPEN_LAST_FILE, '[');
-
- this.bindShortcut(Shortcut.SEARCH, '/');
- }
-
- _isCursorManagerSupportMoveToVisibleLine() {
- // This method is a copy-paste from the
- // method _isIntersectionObserverSupported of gr-cursor-manager.js
- // It is better share this method with gr-cursor-manager,
- // but doing it require a lot if changes instead of 1-line copied code
- return 'IntersectionObserver' in window;
- }
-
_accountChanged(account?: AccountDetailInfo) {
if (!account) return;
@@ -654,7 +502,8 @@
(this.shadowRoot!.querySelector('#keyboardShortcuts') as GrOverlay).open();
}
- _showKeyboardShortcuts(e: CustomKeyboardEvent) {
+ _showKeyboardShortcuts(e: IronKeyboardEvent) {
+ if (this.shortcuts.shouldSuppress(e)) return;
// same shortcut should close the dialog if pressed again
// when dialog is open
this.loadKeyboardShortcutsDialog = true;
@@ -667,9 +516,6 @@
keyboardShortcuts.cancel();
return;
}
- if (this.shouldSuppressKeyboardShortcut(e)) {
- return;
- }
keyboardShortcuts.open();
this._footerHeaderAriaHidden = true;
this._mainAriaHidden = true;
diff --git a/polygerrit-ui/app/elements/gr-app-types.ts b/polygerrit-ui/app/elements/gr-app-types.ts
index e5096c0..6c8bdb9 100644
--- a/polygerrit-ui/app/elements/gr-app-types.ts
+++ b/polygerrit-ui/app/elements/gr-app-types.ts
@@ -52,20 +52,21 @@
groupId: GroupId;
}
-export interface AppElementAdminParams {
+export interface ListViewParams {
+ filter?: string | null;
+ offset?: number | string;
+}
+
+export interface AppElementAdminParams extends ListViewParams {
view: GerritView.ADMIN;
adminView: string;
- offset?: string | number;
- filter?: string | null;
openCreateModal?: boolean;
}
-export interface AppElementRepoParams {
+export interface AppElementRepoParams extends ListViewParams {
view: GerritView.REPO;
detail?: RepoDetailView;
repo: RepoName;
- offset?: string | number;
- filter?: string | null;
}
export interface AppElementDocSearchParams {
diff --git a/polygerrit-ui/app/elements/plugins/gr-plugin-host/gr-plugin-host.ts b/polygerrit-ui/app/elements/plugins/gr-plugin-host/gr-plugin-host.ts
index 988366c..7fee4a0 100644
--- a/polygerrit-ui/app/elements/plugins/gr-plugin-host/gr-plugin-host.ts
+++ b/polygerrit-ui/app/elements/plugins/gr-plugin-host/gr-plugin-host.ts
@@ -37,7 +37,7 @@
getPluginLoader().loadPlugins(pluginsPending);
}
- updated(changedProperties: PropertyValues<GrPluginHost>) {
+ override updated(changedProperties: PropertyValues<GrPluginHost>) {
if (changedProperties.has('config') && this.config) {
this._configChanged(this.config);
}
diff --git a/polygerrit-ui/app/elements/plugins/gr-popup-interface/gr-popup-interface.ts b/polygerrit-ui/app/elements/plugins/gr-popup-interface/gr-popup-interface.ts
index 8ed6611..45a93bf 100644
--- a/polygerrit-ui/app/elements/plugins/gr-popup-interface/gr-popup-interface.ts
+++ b/polygerrit-ui/app/elements/plugins/gr-popup-interface/gr-popup-interface.ts
@@ -51,6 +51,11 @@
return dom(this.popup) as unknown as HTMLElement;
}
+ appendContent(el: HTMLElement) {
+ if (!this.popup) throw new Error('popup element not (yet) available');
+ this.popup.appendChild(el);
+ }
+
/**
* Opens the popup, inserts it into DOM over current UI.
* Creates the popup if not previously created. Creates popup content element,
diff --git a/polygerrit-ui/app/elements/plugins/gr-theme-api/gr-custom-plugin-header.ts b/polygerrit-ui/app/elements/plugins/gr-theme-api/gr-custom-plugin-header.ts
index f913cf6..6580ad6 100644
--- a/polygerrit-ui/app/elements/plugins/gr-theme-api/gr-custom-plugin-header.ts
+++ b/polygerrit-ui/app/elements/plugins/gr-theme-api/gr-custom-plugin-header.ts
@@ -14,6 +14,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
+/* eslint-disable lit/no-legacy-template-syntax,lit/prefer-static-styles */
import {PolymerElement} from '@polymer/polymer/polymer-element';
import {html} from '@polymer/polymer/lib/utils/html-tag';
import {customElement, property} from '@polymer/decorators';
diff --git a/polygerrit-ui/app/elements/settings/gr-account-info/gr-account-info.ts b/polygerrit-ui/app/elements/settings/gr-account-info/gr-account-info.ts
index 536beb4..bd6835c 100644
--- a/polygerrit-ui/app/elements/settings/gr-account-info/gr-account-info.ts
+++ b/polygerrit-ui/app/elements/settings/gr-account-info/gr-account-info.ts
@@ -26,7 +26,6 @@
import {EditableAccountField} from '../../../constants/constants';
import {appContext} from '../../../services/app-context';
import {fireEvent} from '../../../utils/event-util';
-import {KeydownEvent} from '../../../types/events';
@customElement('gr-account-info')
export class GrAccountInfo extends PolymerElement {
@@ -247,7 +246,7 @@
this._hasNameChange = true;
}
- _handleKeydown(e: KeydownEvent) {
+ _handleKeydown(e: KeyboardEvent) {
if (e.keyCode === 13) {
// Enter
e.stopPropagation();
diff --git a/polygerrit-ui/app/elements/settings/gr-account-info/gr-account-info_html.ts b/polygerrit-ui/app/elements/settings/gr-account-info/gr-account-info_html.ts
index 514f00e..51259c8 100644
--- a/polygerrit-ui/app/elements/settings/gr-account-info/gr-account-info_html.ts
+++ b/polygerrit-ui/app/elements/settings/gr-account-info/gr-account-info_html.ts
@@ -56,7 +56,7 @@
<span class="title">Registered</span>
<span class="value">
<gr-date-formatter
- has-tooltip=""
+ withTooltip
date-str="[[_account.registered_on]]"
></gr-date-formatter>
</span>
diff --git a/polygerrit-ui/app/elements/settings/gr-agreements-list/gr-agreements-list.ts b/polygerrit-ui/app/elements/settings/gr-agreements-list/gr-agreements-list.ts
index eca38d9..a972db3 100644
--- a/polygerrit-ui/app/elements/settings/gr-agreements-list/gr-agreements-list.ts
+++ b/polygerrit-ui/app/elements/settings/gr-agreements-list/gr-agreements-list.ts
@@ -41,7 +41,7 @@
});
}
- static get styles() {
+ static override get styles() {
return [
sharedStyles,
css`
@@ -71,7 +71,7 @@
`;
}
- render() {
+ override render() {
return html` <div class="gr-form-styles">
<table id="agreements">
<thead>
diff --git a/polygerrit-ui/app/elements/settings/gr-change-table-editor/gr-change-table-editor.ts b/polygerrit-ui/app/elements/settings/gr-change-table-editor/gr-change-table-editor.ts
index b79e448..96b1ded 100644
--- a/polygerrit-ui/app/elements/settings/gr-change-table-editor/gr-change-table-editor.ts
+++ b/polygerrit-ui/app/elements/settings/gr-change-table-editor/gr-change-table-editor.ts
@@ -15,22 +15,18 @@
* limitations under the License.
*/
import '../../shared/gr-button/gr-button';
-import '../../shared/gr-date-formatter/gr-date-formatter';
import '../../../styles/shared-styles';
import '../../../styles/gr-form-styles';
import {dom, EventApi} from '@polymer/polymer/lib/legacy/polymer.dom';
import {PolymerElement} from '@polymer/polymer/polymer-element';
import {htmlTemplate} from './gr-change-table-editor_html';
-import {ChangeTableMixin} from '../../../mixins/gr-change-table-mixin/gr-change-table-mixin';
import {customElement, property, observe} from '@polymer/decorators';
import {ServerInfo} from '../../../types/common';
import {appContext} from '../../../services/app-context';
-
-// This avoids JSC_DYNAMIC_EXTENDS_WITHOUT_JSDOC closure compiler error.
-const base = ChangeTableMixin(PolymerElement);
+import {columnNames} from '../../change-list/gr-change-list/gr-change-list';
@customElement('gr-change-table-editor')
-export class GrChangeTableEditor extends base {
+export class GrChangeTableEditor extends PolymerElement {
static get template() {
return htmlTemplate;
}
@@ -51,18 +47,33 @@
@observe('serverConfig')
_configChanged(config: ServerInfo) {
- this.defaultColumns = this.getEnabledColumns(
- this.columnNames,
- config,
- this.flagsService.enabledExperiments
+ this.defaultColumns = columnNames.filter(col =>
+ this._isColumnEnabled(col, config, this.flagsService.enabledExperiments)
);
if (!this.displayedColumns) return;
this.displayedColumns = this.displayedColumns.filter(column =>
- this.isColumnEnabled(column, config, this.flagsService.enabledExperiments)
+ this._isColumnEnabled(
+ column,
+ config,
+ this.flagsService.enabledExperiments
+ )
);
}
/**
+ * Is the column disabled by a server config or experiment? For example the
+ * assignee feature might be disabled and thus the corresponding column is
+ * also disabled.
+ *
+ */
+ _isColumnEnabled(column: string, config: ServerInfo, experiments: string[]) {
+ if (!config || !config.change) return true;
+ if (column === 'Assignee') return !!config.change.enable_assignee;
+ if (column === 'Comments') return experiments.includes('comments-column');
+ return true;
+ }
+
+ /**
* Get the list of enabled column names from whichever checkboxes are
* checked (excluding the number checkbox).
*/
@@ -79,6 +90,13 @@
.map(checkbox => checkbox.name);
}
+ _computeIsColumnHidden(columnToCheck?: string, columnsToDisplay?: string[]) {
+ if (!columnsToDisplay || !columnToCheck) {
+ return false;
+ }
+ return !columnsToDisplay.includes(columnToCheck);
+ }
+
/**
* Handle a click on a checkbox container and relay the click to the checkbox it
* contains.
diff --git a/polygerrit-ui/app/elements/settings/gr-change-table-editor/gr-change-table-editor_html.ts b/polygerrit-ui/app/elements/settings/gr-change-table-editor/gr-change-table-editor_html.ts
index a05ec73..e756a20 100644
--- a/polygerrit-ui/app/elements/settings/gr-change-table-editor/gr-change-table-editor_html.ts
+++ b/polygerrit-ui/app/elements/settings/gr-change-table-editor/gr-change-table-editor_html.ts
@@ -74,7 +74,7 @@
type="checkbox"
name="[[item]]"
on-click="_handleTargetClick"
- checked$="[[!isColumnHidden(item, displayedColumns)]]"
+ checked$="[[!_computeIsColumnHidden(item, displayedColumns)]]"
/>
</td>
</tr>
diff --git a/polygerrit-ui/app/elements/settings/gr-change-table-editor/gr-change-table-editor_test.ts b/polygerrit-ui/app/elements/settings/gr-change-table-editor/gr-change-table-editor_test.ts
index 4f61972..4f8d0a0 100644
--- a/polygerrit-ui/app/elements/settings/gr-change-table-editor/gr-change-table-editor_test.ts
+++ b/polygerrit-ui/app/elements/settings/gr-change-table-editor/gr-change-table-editor_test.ts
@@ -117,7 +117,7 @@
test('_getDisplayedColumns', () => {
const enabledColumns = columns.filter(column =>
- element.isColumnEnabled(column, element.serverConfig!, [])
+ element._isColumnEnabled(column, element.serverConfig!, [])
);
assert.deepEqual(element._getDisplayedColumns(), enabledColumns);
const input = queryAndAssert<HTMLInputElement>(
diff --git a/polygerrit-ui/app/elements/settings/gr-http-password/gr-http-password.ts b/polygerrit-ui/app/elements/settings/gr-http-password/gr-http-password.ts
index 6354c9a..59f6a39 100644
--- a/polygerrit-ui/app/elements/settings/gr-http-password/gr-http-password.ts
+++ b/polygerrit-ui/app/elements/settings/gr-http-password/gr-http-password.ts
@@ -75,7 +75,7 @@
return Promise.all(promises);
}
- static get styles() {
+ static override get styles() {
return [
sharedStyles,
formStyles,
@@ -113,9 +113,9 @@
];
}
- render() {
+ override render() {
return html` <div class="gr-form-styles">
- <div ?hidden=${this._passwordUrl}>
+ <div ?hidden=${!!this._passwordUrl}>
<section>
<span class="title">Username</span>
<span class="value">${this._username ?? ''}</span>
@@ -125,7 +125,7 @@
>
</div>
<span ?hidden=${!this._passwordUrl}>
- <a href="${this._passwordUrl}" target="_blank" rel="noopener">
+ <a href="${this._passwordUrl!}" target="_blank" rel="noopener">
Obtain password</a
>
(opens in a new tab)
@@ -134,7 +134,7 @@
<gr-overlay
id="generatedPasswordOverlay"
@iron-overlay-closed=${this._generatedPasswordOverlayClosed}
- with-backdrop=""
+ with-backdrop
>
<div class="gr-form-styles">
<section id="generatedPasswordDisplay">
diff --git a/polygerrit-ui/app/elements/settings/gr-menu-editor/gr-menu-editor.ts b/polygerrit-ui/app/elements/settings/gr-menu-editor/gr-menu-editor.ts
index ace1e1a..c392a13 100644
--- a/polygerrit-ui/app/elements/settings/gr-menu-editor/gr-menu-editor.ts
+++ b/polygerrit-ui/app/elements/settings/gr-menu-editor/gr-menu-editor.ts
@@ -16,7 +16,6 @@
*/
import '@polymer/iron-input/iron-input';
import '../../shared/gr-button/gr-button';
-import '../../shared/gr-date-formatter/gr-date-formatter';
import '../../../styles/shared-styles';
import '../../../styles/gr-form-styles';
import {dom, EventApi} from '@polymer/polymer/lib/legacy/polymer.dom';
@@ -24,7 +23,6 @@
import {htmlTemplate} from './gr-menu-editor_html';
import {customElement, property} from '@polymer/decorators';
import {TopMenuItemInfo} from '../../../types/common';
-import {KeydownEvent} from '../../../types/events';
@customElement('gr-menu-editor')
export class GrMenuEditor extends PolymerElement {
@@ -91,7 +89,7 @@
return !newName?.length || !newUrl?.length;
}
- _handleInputKeydown(e: KeydownEvent) {
+ _handleInputKeydown(e: KeyboardEvent) {
if (e.keyCode === 13) {
e.stopPropagation();
this._handleAddButton();
diff --git a/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view.ts b/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view.ts
index da4bb0a..3533fd6 100644
--- a/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view.ts
+++ b/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view.ts
@@ -29,7 +29,6 @@
import '../gr-change-table-editor/gr-change-table-editor';
import '../../shared/gr-button/gr-button';
import {GrButton} from '../../shared/gr-button/gr-button';
-import '../../shared/gr-date-formatter/gr-date-formatter';
import '../../shared/gr-diff-preferences/gr-diff-preferences';
import '../../shared/gr-page-nav/gr-page-nav';
import '../../shared/gr-select/gr-select';
@@ -47,7 +46,6 @@
import {PolymerElement} from '@polymer/polymer/polymer-element';
import {htmlTemplate} from './gr-settings-view_html';
import {getDocsBaseUrl} from '../../../utils/url-util';
-import {ChangeTableMixin} from '../../../mixins/gr-change-table-mixin/gr-change-table-mixin';
import {customElement, property, observe} from '@polymer/decorators';
import {AppElementParams} from '../../gr-app-types';
import {GrAccountInfo} from '../gr-account-info/gr-account-info';
@@ -64,7 +62,6 @@
import {GrSshEditor} from '../gr-ssh-editor/gr-ssh-editor';
import {GrGpgEditor} from '../gr-gpg-editor/gr-gpg-editor';
import {GrEmailEditor} from '../gr-email-editor/gr-email-editor';
-import {KeydownEvent} from '../../../types/events';
import {fireAlert, fireTitleChange} from '../../../utils/event-util';
import {appContext} from '../../../services/app-context';
import {GerritView} from '../../../services/router/router-model';
@@ -76,6 +73,7 @@
EmailStrategy,
TimeFormat,
} from '../../../constants/constants';
+import {columnNames} from '../../change-list/gr-change-list/gr-change-list';
const PREFS_SECTION_FIELDS: Array<keyof PreferencesInput> = [
'changes_per_page',
@@ -139,11 +137,8 @@
};
}
-// This avoids JSC_DYNAMIC_EXTENDS_WITHOUT_JSDOC closure compiler error.
-const base = ChangeTableMixin(PolymerElement);
-
@customElement('gr-settings-view')
-export class GrSettingsView extends base {
+export class GrSettingsView extends PolymerElement {
static get template() {
return htmlTemplate;
}
@@ -262,8 +257,10 @@
this._localMenu = this._cloneMenu(prefs.my);
this._localChangeTableColumns =
prefs.change_table.length === 0
- ? this.columnNames
- : this.renameProjectToRepoColumn(prefs.change_table);
+ ? columnNames
+ : prefs.change_table.map(column =>
+ column === 'Project' ? 'Repo' : column
+ );
})
);
@@ -491,7 +488,7 @@
this.$.emailEditor.save();
}
- _handleNewEmailKeydown(e: KeydownEvent) {
+ _handleNewEmailKeydown(e: KeyboardEvent) {
if (e.keyCode === 13) {
// Enter
e.stopPropagation();
diff --git a/polygerrit-ui/app/elements/shared/gr-account-chip/gr-account-chip.ts b/polygerrit-ui/app/elements/shared/gr-account-chip/gr-account-chip.ts
index 66103fd..31c62b1 100644
--- a/polygerrit-ui/app/elements/shared/gr-account-chip/gr-account-chip.ts
+++ b/polygerrit-ui/app/elements/shared/gr-account-chip/gr-account-chip.ts
@@ -128,6 +128,7 @@
override render() {
// To pass CSS mixins for @apply to Polymer components, they need to appear
// in <style> inside the template.
+ /* eslint-disable lit/prefer-static-styles */
const customStyle = html`
<style>
.container {
diff --git a/polygerrit-ui/app/elements/shared/gr-account-label/gr-account-label.ts b/polygerrit-ui/app/elements/shared/gr-account-label/gr-account-label.ts
index f746c29..dabf761 100644
--- a/polygerrit-ui/app/elements/shared/gr-account-label/gr-account-label.ts
+++ b/polygerrit-ui/app/elements/shared/gr-account-label/gr-account-label.ts
@@ -199,25 +199,14 @@
${!this.hideHovercard
? html`<gr-hovercard-account
for="hovercardTarget"
- .account="${account}"
- .change="${change}"
- ?highlight-attention=${highlightAttention}
+ .account=${account}
+ .change=${change}
+ .highlightAttention=${highlightAttention}
.voteableText=${this.voteableText}
></gr-hovercard-account>`
: ''}
${hasAttention
- ? html`<gr-button
- id="attentionButton"
- link=""
- aria-label="Remove user from attention set"
- @click=${this._handleRemoveAttentionClick}
- ?disabled=${!this._computeAttentionButtonEnabled(
- highlightAttention,
- account,
- change,
- this.selected,
- this._selfAccount
- )}
+ ? html` <gr-tooltip-content
?has-tooltip=${this._computeAttentionButtonEnabled(
highlightAttention,
account,
@@ -233,11 +222,25 @@
this.selected,
this._selfAccount
)}"
- ><iron-icon
- class="attention"
- icon="gr-icons:attention"
- ></iron-icon>
- </gr-button>`
+ >
+ <gr-button
+ id="attentionButton"
+ link=""
+ aria-label="Remove user from attention set"
+ @click=${this._handleRemoveAttentionClick}
+ ?disabled=${!this._computeAttentionButtonEnabled(
+ highlightAttention,
+ account,
+ change,
+ this.selected,
+ this._selfAccount
+ )}
+ ><iron-icon
+ class="attention"
+ icon="gr-icons:attention"
+ ></iron-icon>
+ </gr-button>
+ </gr-tooltip-content>`
: ''}
</span>
<span
diff --git a/polygerrit-ui/app/elements/shared/gr-account-link/gr-account-link.ts b/polygerrit-ui/app/elements/shared/gr-account-link/gr-account-link.ts
index b302bee..f0c9106 100644
--- a/polygerrit-ui/app/elements/shared/gr-account-link/gr-account-link.ts
+++ b/polygerrit-ui/app/elements/shared/gr-account-link/gr-account-link.ts
@@ -20,6 +20,7 @@
import {AccountInfo, ChangeInfo} from '../../../types/common';
import {LitElement, css, html} from 'lit';
import {customElement, property} from 'lit/decorators';
+import {ParsedChangeInfo} from '../../../types/types';
@customElement('gr-account-link')
export class GrAccountLink extends LitElement {
@@ -35,7 +36,7 @@
* related features like adding the user as a reviewer.
*/
@property({type: Object})
- change?: ChangeInfo;
+ change?: ChangeInfo | ParsedChangeInfo;
/**
* Should this user be considered to be in the attention set, regardless
@@ -95,7 +96,7 @@
?hideStatus=${this.hideStatus}
?firstName=${this.firstName}
.voteableText=${this.voteableText}
- part="gr-account-link-text => gr-account-label-text"
+ exportparts="gr-account-label-text: gr-account-link-text"
>
</gr-account-label>
</a>
diff --git a/polygerrit-ui/app/elements/shared/gr-account-list/gr-account-list.ts b/polygerrit-ui/app/elements/shared/gr-account-list/gr-account-list.ts
index c5d1f03..d97e38e 100644
--- a/polygerrit-ui/app/elements/shared/gr-account-list/gr-account-list.ts
+++ b/polygerrit-ui/app/elements/shared/gr-account-list/gr-account-list.ts
@@ -39,7 +39,6 @@
import {PaperInputElementExt} from '../../../types/types';
import {fireAlert} from '../../../utils/event-util';
import {accountOrGroupKey} from '../../../utils/account-util';
-import {KeydownEvent} from '../../../types/events';
const VALID_EMAIL_ALERT = 'Please input a valid email.';
@@ -360,7 +359,7 @@
}
}
- _handleChipKeydown(e: KeydownEvent) {
+ _handleChipKeydown(e: KeyboardEvent) {
const chip = e.target as GrAccountChip;
const chips = this.accountChips;
const index = chips.indexOf(chip);
diff --git a/polygerrit-ui/app/elements/shared/gr-alert/gr-alert.ts b/polygerrit-ui/app/elements/shared/gr-alert/gr-alert.ts
index 895c99b..fa547dc 100644
--- a/polygerrit-ui/app/elements/shared/gr-alert/gr-alert.ts
+++ b/polygerrit-ui/app/elements/shared/gr-alert/gr-alert.ts
@@ -30,7 +30,7 @@
@customElement('gr-alert')
export class GrAlert extends LitElement {
- static get styles() {
+ static override get styles() {
return [
sharedStyles,
css`
@@ -43,7 +43,6 @@
bottom: 1.25rem;
border-radius: var(--border-radius);
box-shadow: var(--elevation-level-2);
- color: var(--tooltip-text-color);
left: 1.25rem;
position: fixed;
transform: translateY(5rem);
@@ -73,11 +72,10 @@
vertical-align: bottom;
word-break: break-all;
}
- .action {
- color: var(--link-color);
- font-weight: var(--font-weight-bold);
+ gr-button.action {
+ --text-color: var(--tooltip-button-text-color);
+ --gr-button-padding: 0 var(--spacing-s);
margin-left: var(--spacing-l);
- text-decoration: none;
}
`,
];
@@ -93,18 +91,9 @@
>`;
}
- render() {
- // To pass CSS mixins for @apply to Polymer components, they need to appear
- // in <style> inside the template.
- const style = html`<style>
- .action {
- --gr-button: {
- padding: 0;
- }
- }
- </style>`;
+ override render() {
const {text, actionText} = this;
- return html`${style}
+ return html`
<div class="content-wrapper">
<span class="text">${text}</span>
<gr-button
@@ -115,7 +104,8 @@
>${actionText}
</gr-button>
${this.renderDismissButton()}
- </div> `;
+ </div>
+ `;
}
/**
diff --git a/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete.ts b/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete.ts
index 3f52650..524b197 100644
--- a/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete.ts
+++ b/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete.ts
@@ -26,9 +26,10 @@
import {property, customElement, observe} from '@polymer/decorators';
import {GrAutocompleteDropdown} from '../gr-autocomplete-dropdown/gr-autocomplete-dropdown';
import {PaperInputElementExt} from '../../../types/types';
-import {CustomKeyboardEvent} from '../../../types/events';
import {fireEvent} from '../../../utils/event-util';
import {debounce, DelayedTask} from '../../../utils/async-util';
+import {PropertyType} from '../../../types/common';
+import {modifierPressed} from '../../../utils/dom-util';
const TOKENIZE_REGEX = /(?:[^\s"]+|"[^"]*")+/g;
const DEBOUNCE_WAIT_MS = 200;
@@ -40,9 +41,9 @@
};
}
-export type AutocompleteQuery = (
+export type AutocompleteQuery<T = string> = (
text: string
-) => Promise<AutocompleteSuggestion[]>;
+) => Promise<Array<AutocompleteSuggestion<T>>>;
declare global {
interface HTMLElementTagNameMap {
@@ -50,11 +51,11 @@
}
}
-export interface AutocompleteSuggestion {
+export interface AutocompleteSuggestion<T = string> {
name?: string;
label?: string;
- value?: string;
- text?: string;
+ value?: T;
+ text?: T;
}
export interface AutocompleteCommitEventDetail {
@@ -102,7 +103,7 @@
*
*/
@property({type: Object})
- query: AutocompleteQuery = () => Promise.resolve([]);
+ query?: AutocompleteQuery = () => Promise.resolve([]);
/**
* The number of characters that must be typed before suggestions are
@@ -298,6 +299,12 @@
if (this._disableSuggestions) {
return;
}
+
+ const query = this.query;
+ if (!query) {
+ return;
+ }
+
if (text.length < threshold) {
this.value = '';
return;
@@ -308,7 +315,7 @@
}
const update = () => {
- this.query(text).then(suggestions => {
+ query(text).then(suggestions => {
if (text !== this.text) {
// Late response.
return;
@@ -351,7 +358,7 @@
* _handleKeydown used for key handling in the this.$.input AND all child
* autocomplete options.
*/
- _handleKeydown(e: CustomKeyboardEvent) {
+ _handleKeydown(e: KeyboardEvent) {
this._focused = true;
switch (e.keyCode) {
case 38: // Up
@@ -376,7 +383,7 @@
}
break;
case 13: // Enter
- if (this.modifierPressed(e)) {
+ if (modifierPressed(e)) {
break;
}
e.preventDefault();
@@ -505,3 +512,24 @@
return showSearchIcon ? 'showSearchIcon' : '';
}
}
+
+/**
+ * Often gr-autocomplete is used for BranchName, RepoName, etc...
+ * GrTypedAutocomplete allows to define more precise typing in templates.
+ * For example, instead of
+ * $: {
+ * branchSelect: GrAutocomplete
+ * }
+ * you can write
+ * $: {
+ * branchSelect: GrTypedAutocomplete<BranchName>
+ * }
+ * And later user $.branchSelect.text without type conversion to BranchName.
+ */
+export interface GrTypedAutocomplete<
+ T extends PropertyType<GrAutocomplete, 'text'>
+> extends GrAutocomplete {
+ text: T;
+ value: T;
+ query?: AutocompleteQuery<T>;
+}
diff --git a/polygerrit-ui/app/elements/shared/gr-button/gr-button.ts b/polygerrit-ui/app/elements/shared/gr-button/gr-button.ts
index 1ece10a..8dc23e2 100644
--- a/polygerrit-ui/app/elements/shared/gr-button/gr-button.ts
+++ b/polygerrit-ui/app/elements/shared/gr-button/gr-button.ts
@@ -15,21 +15,13 @@
* limitations under the License.
*/
import '@polymer/paper-button/paper-button';
-import '../../../styles/shared-styles';
-import '../../../styles/gr-voting-styles';
-import {PolymerElement} from '@polymer/polymer/polymer-element';
-import {customElement, property, computed, observe} from '@polymer/decorators';
-import {htmlTemplate} from './gr-button_html';
-import {TooltipMixin} from '../../../mixins/gr-tooltip-mixin/gr-tooltip-mixin';
-import {
- PolymerEvent,
- getEventPath,
- getKeyboardEvent,
- isModifierPressed,
-} from '../../../utils/dom-util';
+import {spinnerStyles} from '../../../styles/gr-spinner-styles';
+import {votingStyles} from '../../../styles/gr-voting-styles';
+import {css, html, LitElement, PropertyValues} from 'lit';
+import {customElement, property} from 'lit/decorators';
+import {getEventPath, modifierPressed} from '../../../utils/dom-util';
import {appContext} from '../../../services/app-context';
import {ReportingService} from '../../../services/gr-reporting/gr-reporting';
-import {CustomKeyboardEvent} from '../../../types/events';
declare global {
interface HTMLElementTagNameMap {
@@ -37,87 +29,208 @@
}
}
-// This avoids JSC_DYNAMIC_EXTENDS_WITHOUT_JSDOC closure compiler error.
-const base = TooltipMixin(PolymerElement);
-
@customElement('gr-button')
-export class GrButton extends base {
- static get template() {
- return htmlTemplate;
- }
+export class GrButton extends LitElement {
+ private readonly reporting: ReportingService = appContext.reportingService;
/**
* Should this button be rendered as a vote chip? Then we are applying
* the .voteChip class (see gr-voting-styles) to the paper-button.
*/
- @property({type: Boolean, reflectToAttribute: true})
+ @property({type: Boolean, reflect: true})
voteChip = false;
- @property({type: Boolean, reflectToAttribute: true})
- downArrow = false;
-
- @property({type: Boolean, reflectToAttribute: true})
- link = false;
-
- @property({type: Boolean})
- noUppercase = false;
-
- @property({type: Boolean, reflectToAttribute: true})
- loading = false;
-
- @property({type: Boolean, reflectToAttribute: true})
- disabled: boolean | null = null;
-
- @property({type: String})
- tooltip = '';
-
// Note: don't assign a value to this, since constructor is called
// after created, the initial value maybe overridden by this
- @property({type: String})
- _initialTabindex?: string;
+ private initialTabindex?: string;
- @computed('disabled', 'loading')
- get _disabled() {
- return this.disabled || this.loading;
+ @property({type: Boolean, reflect: true, attribute: 'down-arrow'})
+ downArrow = false;
+
+ @property({type: Boolean, reflect: true})
+ link = false;
+
+ @property({type: Boolean, reflect: true})
+ loading = false;
+
+ @property({type: Boolean, reflect: true})
+ disabled: boolean | null = null;
+
+ static override get styles() {
+ return [
+ votingStyles,
+ spinnerStyles,
+ css`
+ /* general styles for all buttons */
+ :host {
+ --background-color: var(
+ --button-background-color,
+ var(--default-button-background-color)
+ );
+ --text-color: var(
+ --gr-button-text-color,
+ var(--default-button-text-color)
+ );
+ display: inline-block;
+ position: relative;
+ }
+ :host([hidden]) {
+ display: none;
+ }
+ :host([no-uppercase]) paper-button {
+ text-transform: none;
+ }
+ paper-button {
+ /* paper-button sets this to anti-aliased, which appears different than
+ bold font elsewhere on macOS. */
+ -webkit-font-smoothing: initial;
+ align-items: center;
+ background-color: var(--background-color);
+ color: var(--text-color);
+ display: flex;
+ font-family: inherit;
+ justify-content: center;
+ margin: var(--margin, 0);
+ min-width: var(--border, 0);
+ padding: var(--gr-button-padding, var(--spacing-s) var(--spacing-m));
+ }
+ paper-button[elevation='1'] {
+ box-shadow: var(--elevation-level-1);
+ }
+ paper-button[elevation='2'] {
+ box-shadow: var(--elevation-level-2);
+ }
+ paper-button[elevation='3'] {
+ box-shadow: var(--elevation-level-3);
+ }
+ paper-button[elevation='4'] {
+ box-shadow: var(--elevation-level-4);
+ }
+ paper-button[elevation='5'] {
+ box-shadow: var(--elevation-level-5);
+ }
+ paper-button:hover {
+ background: linear-gradient(rgba(0, 0, 0, 0.12), rgba(0, 0, 0, 0.12)),
+ var(--background-color);
+ }
+
+ /* Some mobile browsers treat focused element as hovered element.
+ As a result, element remains hovered after click (has grey background in default theme).
+ Use @media (hover:none) to remove background if
+ user's primary input mechanism can't hover over elements.
+ See: https://developer.mozilla.org/en-US/docs/Web/CSS/@media/hover
+
+ Note 1: not all browsers support this media query
+ (see https://caniuse.com/#feat=css-media-interaction).
+ If browser doesn't support it, then the whole content of @media .. is ignored.
+ This is why the default behavior is placed outside of @media.
+ */
+ @media (hover: none) {
+ paper-button:hover {
+ background: transparent;
+ }
+ }
+
+ :host([primary]) {
+ --background-color: var(--primary-button-background-color);
+ --text-color: var(--primary-button-text-color);
+ }
+ :host([link][primary]) {
+ --text-color: var(--primary-button-background-color);
+ }
+
+ /* Keep below color definition for primary so that this takes precedence
+ when disabled. */
+ :host([disabled]),
+ :host([loading]) {
+ --background-color: var(--disabled-button-background-color);
+ --text-color: var(--deemphasized-text-color);
+ cursor: default;
+ }
+
+ /* Styles for link buttons specifically */
+ :host([link]) {
+ --background-color: transparent;
+ --margin: 0;
+ }
+ :host([link]) paper-button {
+ padding: var(--gr-button-padding, var(--spacing-s));
+ }
+ :host([disabled][link]),
+ :host([loading][link]) {
+ --background-color: transparent;
+ --text-color: var(--deemphasized-text-color);
+ cursor: default;
+ }
+
+ /* Styles for the optional down arrow */
+ :host(:not([down-arrow])) .downArrow {
+ display: none;
+ }
+ :host([down-arrow]) .downArrow {
+ border-top: 0.36em solid #ccc;
+ border-left: 0.36em solid transparent;
+ border-right: 0.36em solid transparent;
+ margin-bottom: var(--spacing-xxs);
+ margin-left: var(--spacing-m);
+ transition: border-top-color 200ms;
+ }
+ :host([down-arrow]) paper-button:hover .downArrow {
+ border-top-color: var(--deemphasized-text-color);
+ }
+ `,
+ ];
}
- @property({
- computed: 'computeAriaDisabled(disabled, loading)',
- reflectToAttribute: true,
- type: String,
- })
- ariaDisabled!: string;
-
- computeAriaDisabled() {
- return this._disabled ? 'true' : 'false';
+ override render() {
+ return html`<paper-button
+ ?raised="${!this.link}"
+ ?disabled="${this.disabled || this.loading}"
+ role="button"
+ tabindex="-1"
+ part="paper-button"
+ class="${this.voteChip ? 'voteChip' : ''}"
+ >
+ ${this.loading ? html`<span class="loadingSpin"></span>` : ''}
+ <slot></slot>
+ <i class="downArrow"></i>
+ </paper-button>`;
}
- computePaperButtonClass(voteChip?: boolean) {
- return voteChip ? 'voteChip' : '';
- }
-
- private readonly reporting: ReportingService = appContext.reportingService;
-
constructor() {
super();
- this._initialTabindex = this.getAttribute('tabindex') || '0';
- // TODO(TS): try avoid using unknown
- this.addEventListener('click', e =>
- this._handleAction(e as unknown as PolymerEvent)
- );
- this.addEventListener('keydown', e =>
- this._handleKeydown(e as unknown as CustomKeyboardEvent)
- );
+ this.initialTabindex = this.getAttribute('tabindex') || '0';
+ this.addEventListener('click', e => this._handleAction(e));
+ this.addEventListener('keydown', e => this._handleKeydown(e));
}
- override ready() {
- super.ready();
- this._ensureAttribute('role', 'button');
- this._ensureAttribute('tabindex', '0');
+ override updated(changedProperties: PropertyValues) {
+ if (changedProperties.has('disabled')) {
+ this.setAttribute(
+ 'tabindex',
+ this.disabled ? '-1' : this.initialTabindex || '0'
+ );
+ }
+ if (changedProperties.has('loading') || changedProperties.has('disabled')) {
+ this.setAttribute(
+ 'aria-disabled',
+ this.disabled || this.loading ? 'true' : 'false'
+ );
+ }
}
- _handleAction(e: PolymerEvent) {
- if (this._disabled) {
+ override connectedCallback() {
+ super.connectedCallback();
+ if (!this.getAttribute('role')) {
+ this.setAttribute('role', 'button');
+ }
+ if (!this.getAttribute('tabindex')) {
+ this.setAttribute('tabindex', '0');
+ }
+ }
+
+ _handleAction(e: MouseEvent) {
+ if (this.disabled || this.loading) {
e.preventDefault();
e.stopPropagation();
e.stopImmediatePropagation();
@@ -127,20 +240,8 @@
this.reporting.reportInteraction('button-click', {path: getEventPath(e)});
}
- @observe('disabled')
- _disabledChanged(disabled: boolean) {
- this.setAttribute(
- 'tabindex',
- disabled ? '-1' : this._initialTabindex || '0'
- );
- this.updateStyles();
- }
-
- _handleKeydown(e: CustomKeyboardEvent) {
- if (isModifierPressed(e)) {
- return;
- }
- e = getKeyboardEvent(e);
+ _handleKeydown(e: KeyboardEvent) {
+ if (modifierPressed(e)) return;
// Handle `enter`, `space`.
if (e.keyCode === 13 || e.keyCode === 32) {
e.preventDefault();
diff --git a/polygerrit-ui/app/elements/shared/gr-button/gr-button_html.ts b/polygerrit-ui/app/elements/shared/gr-button/gr-button_html.ts
deleted file mode 100644
index 22ec2f4..0000000
--- a/polygerrit-ui/app/elements/shared/gr-button/gr-button_html.ts
+++ /dev/null
@@ -1,188 +0,0 @@
-/**
- * @license
- * Copyright (C) 2020 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 {html} from '@polymer/polymer/lib/utils/html-tag';
-
-export const htmlTemplate = html`
- <style include="gr-voting-styles">
- /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
- </style>
- <style include="gr-spinner-styles">
- /* general styles for all buttons */
- :host {
- --background-color: var(
- --button-background-color,
- var(--default-button-background-color)
- );
- --text-color: var(--default-button-text-color);
- display: inline-block;
- position: relative;
- }
- :host([hidden]) {
- display: none;
- }
- :host([no-uppercase]) paper-button {
- text-transform: none;
- }
- paper-button {
- /* The next lines contains a copy of paper-button style.
- Without a copy, the @apply works incorrectly with Polymer 2.
- @apply is deprecated and is not recommended to use. It is expected
- that @apply will be replaced with the ::part CSS pseudo-element.
- After replacement copied lines can be removed.
- */
- @apply --layout-inline;
- @apply --layout-center-center;
- position: relative;
- box-sizing: border-box;
- min-width: 5.14em;
- margin: 0 0.29em;
- background: transparent;
- -webkit-tap-highlight-color: rgba(0, 0, 0, 0);
- -webkit-tap-highlight-color: transparent;
- font: inherit;
- text-transform: uppercase;
- outline-width: 0;
- border-top-left-radius: var(--border-radius);
- border-top-right-radius: var(--border-radius);
- border-bottom-right-radius: var(--border-radius);
- border-bottom-left-radius: var(--border-radius);
- -moz-user-select: none;
- -ms-user-select: none;
- -webkit-user-select: none;
- user-select: none;
- cursor: pointer;
- z-index: 0;
- padding: var(--spacing-m);
-
- @apply --paper-font-common-base;
- @apply --paper-button;
- /* End of copy*/
-
- /* paper-button sets this to anti-aliased, which appears different than
- bold font elsewhere on macOS. */
- -webkit-font-smoothing: initial;
- align-items: center;
- background-color: var(--background-color);
- color: var(--text-color);
- display: flex;
- font-family: inherit;
- justify-content: center;
- margin: var(--margin, 0);
- min-width: var(--border, 0);
- padding: var(--padding, 4px 8px);
- @apply --gr-button;
- }
- /* https://github.com/PolymerElements/paper-button/blob/2.x/paper-button.html */
- /* BEGIN: Copy from paper-button */
- paper-button[elevation='1'] {
- @apply --paper-material-elevation-1;
- }
- paper-button[elevation='2'] {
- @apply --paper-material-elevation-2;
- }
- paper-button[elevation='3'] {
- @apply --paper-material-elevation-3;
- }
- paper-button[elevation='4'] {
- @apply --paper-material-elevation-4;
- }
- paper-button[elevation='5'] {
- @apply --paper-material-elevation-5;
- }
- /* END: Copy from paper-button */
- paper-button:hover {
- background: linear-gradient(rgba(0, 0, 0, 0.12), rgba(0, 0, 0, 0.12)),
- var(--background-color);
- }
-
- /* Some mobile browsers treat focused element as hovered element.
- As a result, element remains hovered after click (has grey background in default theme).
- Use @media (hover:none) to remove background if
- user's primary input mechanism can't hover over elements.
- See: https://developer.mozilla.org/en-US/docs/Web/CSS/@media/hover
-
- Note 1: not all browsers support this media query
- (see https://caniuse.com/#feat=css-media-interaction).
- If browser doesn't support it, then the whole content of @media .. is ignored.
- This is why the default behavior is placed outside of @media.
- */
- @media (hover: none) {
- paper-button:hover {
- background: transparent;
- }
- }
-
- :host([primary]) {
- --background-color: var(--primary-button-background-color);
- --text-color: var(--primary-button-text-color);
- }
- :host([link][primary]) {
- --text-color: var(--primary-button-background-color);
- }
-
- /* Keep below color definition for primary so that this takes precedence
- when disabled. */
- :host([disabled]),
- :host([loading]) {
- --background-color: var(--disabled-button-background-color);
- --text-color: var(--deemphasized-text-color);
- cursor: default;
- }
-
- /* Styles for link buttons specifically */
- :host([link]) {
- --background-color: transparent;
- --margin: 0;
- --padding: var(--spacing-s);
- }
- :host([disabled][link]),
- :host([loading][link]) {
- --background-color: transparent;
- --text-color: var(--deemphasized-text-color);
- cursor: default;
- }
-
- /* Styles for the optional down arrow */
- :host(:not([down-arrow])) .downArrow {
- display: none;
- }
- :host([down-arrow]) .downArrow {
- border-top: 0.36em solid #ccc;
- border-left: 0.36em solid transparent;
- border-right: 0.36em solid transparent;
- margin-bottom: var(--spacing-xxs);
- margin-left: var(--spacing-m);
- transition: border-top-color 200ms;
- }
- :host([down-arrow]) paper-button:hover .downArrow {
- border-top-color: var(--deemphasized-text-color);
- }
- </style>
- <paper-button
- raised="[[!link]]"
- disabled="[[_disabled]]"
- tabindex="-1"
- part="paper-button"
- class$="[[computePaperButtonClass(voteChip)]]"
- >
- <template is="dom-if" if="[[loading]]">
- <span class="loadingSpin"></span>
- </template>
- <slot></slot>
- <i class="downArrow"></i>
- </paper-button>
-`;
diff --git a/polygerrit-ui/app/elements/shared/gr-button/gr-button_test.ts b/polygerrit-ui/app/elements/shared/gr-button/gr-button_test.ts
index f0f122a..0149bd5 100644
--- a/polygerrit-ui/app/elements/shared/gr-button/gr-button_test.ts
+++ b/polygerrit-ui/app/elements/shared/gr-button/gr-button_test.ts
@@ -17,6 +17,7 @@
import * as MockInteractions from '@polymer/iron-test-helpers/mock-interactions';
import '../../../test/common-test-setup-karma';
+import './gr-button';
import {addListener} from '@polymer/polymer/lib/utils/gestures';
import {appContext} from '../../../services/app-context';
import {html} from '@polymer/polymer/lib/utils/html-tag';
@@ -49,23 +50,26 @@
return spy;
};
- setup(() => {
+ setup(async () => {
element = basicFixture.instantiate();
+ await element.updateComplete;
});
- test('disabled is set by disabled', () => {
+ test('disabled is set by disabled', async () => {
const paperBtn = queryAndAssert<PaperButtonElement>(
element,
'paper-button'
);
assert.isFalse(paperBtn.disabled);
element.disabled = true;
+ await element.updateComplete;
assert.isTrue(paperBtn.disabled);
element.disabled = false;
+ await element.updateComplete;
assert.isFalse(paperBtn.disabled);
});
- test('loading set from listener', () => {
+ test('loading set from listener', async () => {
let resolve: Function;
element.addEventListener('click', e => {
const target = e.target as HTMLElement;
@@ -78,36 +82,44 @@
);
assert.isFalse(paperBtn.disabled);
MockInteractions.tap(element);
+ await element.updateComplete;
assert.isTrue(paperBtn.disabled);
assert.isTrue(element.hasAttribute('loading'));
resolve!();
- flush();
+ await element.updateComplete;
assert.isFalse(paperBtn.disabled);
assert.isFalse(element.hasAttribute('loading'));
});
- test('tabindex should be -1 if disabled', () => {
+ test('tabindex should be -1 if disabled', async () => {
element.disabled = true;
- assert.isTrue(element.getAttribute('tabindex') === '-1');
+ await element.updateComplete;
+ assert.equal(element.getAttribute('tabindex'), '-1');
});
// Regression tests for Issue: 11969
- test('tabindex should be reset to 0 if enabled', () => {
+ test('tabindex should be reset to 0 if enabled', async () => {
element.disabled = false;
+ await element.updateComplete;
assert.equal(element.getAttribute('tabindex'), '0');
element.disabled = true;
+ await element.updateComplete;
assert.equal(element.getAttribute('tabindex'), '-1');
element.disabled = false;
+ await element.updateComplete;
assert.equal(element.getAttribute('tabindex'), '0');
});
- test('tabindex should be preserved', () => {
+ test('tabindex should be preserved', async () => {
const tabIndexElement = tabindexFixture.instantiate() as GrButton;
tabIndexElement.disabled = false;
+ await element.updateComplete;
assert.equal(tabIndexElement.getAttribute('tabindex'), '3');
tabIndexElement.disabled = true;
+ await element.updateComplete;
assert.equal(tabIndexElement.getAttribute('tabindex'), '-1');
tabIndexElement.disabled = false;
+ await element.updateComplete;
assert.equal(tabIndexElement.getAttribute('tabindex'), '3');
});
@@ -152,8 +164,9 @@
}
suite('disabled', () => {
- setup(() => {
+ setup(async () => {
element.disabled = true;
+ await element.updateComplete;
});
for (const eventName of ['tap', 'click']) {
diff --git a/polygerrit-ui/app/elements/shared/gr-change-star/gr-change-star.ts b/polygerrit-ui/app/elements/shared/gr-change-star/gr-change-star.ts
index 5069ba4..a23621e 100644
--- a/polygerrit-ui/app/elements/shared/gr-change-star/gr-change-star.ts
+++ b/polygerrit-ui/app/elements/shared/gr-change-star/gr-change-star.ts
@@ -21,6 +21,11 @@
import {customElement, property} from '@polymer/decorators';
import {ChangeInfo} from '../../../types/common';
import {fireAlert} from '../../../utils/event-util';
+import {
+ Shortcut,
+ ShortcutSection,
+} from '../../../services/shortcuts/shortcuts-config';
+import {appContext} from '../../../services/app-context';
declare global {
interface HTMLElementTagNameMap {
@@ -48,6 +53,8 @@
@property({type: Object, notify: true})
change?: ChangeInfo;
+ private readonly shortcuts = appContext.shortcutsService;
+
_computeStarClass(starred?: boolean) {
return starred ? 'active' : '';
}
@@ -83,4 +90,8 @@
})
);
}
+
+ createTitle(shortcutName: Shortcut, section: ShortcutSection) {
+ return this.shortcuts.createTitle(shortcutName, section);
+ }
}
diff --git a/polygerrit-ui/app/elements/shared/gr-change-status/gr-change-status_html.ts b/polygerrit-ui/app/elements/shared/gr-change-status/gr-change-status_html.ts
index 2ca2744b..455bd4e 100644
--- a/polygerrit-ui/app/elements/shared/gr-change-status/gr-change-status_html.ts
+++ b/polygerrit-ui/app/elements/shared/gr-change-status/gr-change-status_html.ts
@@ -80,8 +80,8 @@
}
</style>
<gr-tooltip-content
- has-tooltip=""
- position-below=""
+ has-tooltip
+ position-below
title="[[tooltipText]]"
max-width="40em"
>
@@ -101,9 +101,8 @@
</a>
</template>
<template is="dom-if" if="[[!hasStatusLink(revertedChange, resolveWeblinks, status)]]">
- <div class="chip" aria-label$="Label: [[status]]">
- [[_computeStatusString(status)]]
- </div>
+ <div class="chip" aria-label$="Label: [[status]]"
+ >[[_computeStatusString(status)]]</div>
</template>
</gr-tooltip-content>
</span>
diff --git a/polygerrit-ui/app/elements/shared/gr-comment-thread/gr-comment-thread.ts b/polygerrit-ui/app/elements/shared/gr-comment-thread/gr-comment-thread.ts
index 82f6b46..f1b74ac 100644
--- a/polygerrit-ui/app/elements/shared/gr-comment-thread/gr-comment-thread.ts
+++ b/polygerrit-ui/app/elements/shared/gr-comment-thread/gr-comment-thread.ts
@@ -14,9 +14,11 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
+import '../../../styles/gr-a11y-styles';
import '../../../styles/shared-styles';
import '../gr-comment/gr-comment';
import '../../diff/gr-diff/gr-diff';
+import '../gr-copy-clipboard/gr-copy-clipboard';
import {PolymerElement} from '@polymer/polymer/polymer-element';
import {htmlTemplate} from './gr-comment-thread_html';
import {KeyboardShortcutMixin} from '../../../mixins/keyboard-shortcut-mixin/keyboard-shortcut-mixin';
@@ -29,6 +31,7 @@
UIComment,
UIDraft,
UIRobot,
+ DraftInfo,
} from '../../../utils/comment-util';
import {GerritNav} from '../../core/gr-navigation/gr-navigation';
import {appContext} from '../../../services/app-context';
@@ -51,12 +54,12 @@
} from '../../../types/common';
import {GrComment} from '../gr-comment/gr-comment';
import {PolymerDeepPropertyChange} from '@polymer/polymer/interfaces';
-import {CustomKeyboardEvent} from '../../../types/events';
+import {IronKeyboardEvent} from '../../../types/events';
import {LineNumber, FILE} from '../../diff/gr-diff/gr-diff-line';
import {GrButton} from '../gr-button/gr-button';
import {KnownExperimentId} from '../../../services/flags/flags';
import {DiffInfo, DiffPreferencesInfo} from '../../../types/diff';
-import {RenderPreferences} from '../../../api/diff';
+import {DiffLayer, RenderPreferences} from '../../../api/diff';
import {
check,
assertIsDefined,
@@ -204,8 +207,8 @@
@property({type: Object})
_selfAccount?: AccountDetailInfo;
- @property({type: Boolean})
- disableTokenHighlighting = false;
+ @property({type: Array})
+ layers: DiffLayer[] = [];
get keyBindings() {
return {
@@ -225,15 +228,15 @@
readonly restApiService = appContext.restApiService;
+ private readonly shortcuts = appContext.shortcutsService;
+
constructor() {
super();
this.addEventListener('comment-update', e =>
this._handleCommentUpdate(e as CustomEvent)
);
appContext.restApiService.getPreferences().then(prefs => {
- if (prefs?.disable_token_highlighting) {
- this.disableTokenHighlighting = prefs.disable_token_highlighting;
- }
+ this._initLayers(!!prefs?.disable_token_highlighting);
});
}
@@ -350,7 +353,8 @@
return GerritNav.getUrlForComment(changeNum, projectName, id);
}
- getHighlightRange() {
+ /** The parameter is for triggering re-computation only. */
+ getHighlightRange(_: unknown) {
const comment = this.comments?.[0];
if (!comment) return undefined;
if (comment.range) return comment.range;
@@ -365,26 +369,23 @@
return undefined;
}
- _getLayers(diff?: DiffInfo) {
- if (!diff) return [];
- const layers = [];
+ _initLayers(disableTokenHighlighting: boolean) {
if (
this.flagsService.isEnabled(KnownExperimentId.TOKEN_HIGHLIGHTING) &&
- !this.disableTokenHighlighting
+ !disableTokenHighlighting
) {
- layers.push(new TokenHighlightLayer(this));
+ this.layers.push(new TokenHighlightLayer(this));
}
- layers.push(this.syntaxLayer);
- return layers;
+ this.layers.push(this.syntaxLayer);
}
_getUrlForViewDiff(
comments: UIComment[],
changeNum?: NumericChangeId,
projectName?: RepoName
- ) {
- if (!changeNum) return;
- if (!projectName) return;
+ ): string {
+ if (!changeNum) return '';
+ if (!projectName) return '';
check(comments.length > 0, 'comment not found');
return GerritNav.getUrlForComment(changeNum, projectName, comments[0].id!);
}
@@ -430,7 +431,7 @@
});
}
- _isPatchsetLevelComment(path: string) {
+ _isPatchsetLevelComment(path?: string) {
return path === SpecialFilePath.PATCHSET_LEVEL_COMMENTS;
}
@@ -439,7 +440,7 @@
return this.showPortedComment && comment.id === this._orderedComments[0].id;
}
- _computeDisplayPath(path: string) {
+ _computeDisplayPath(path?: string) {
const displayPath = computeDisplayPath(path);
if (displayPath === SpecialFilePath.PATCHSET_LEVEL_COMMENTS) {
return 'Patchset';
@@ -498,8 +499,8 @@
return this._orderedComments[this._orderedComments.length - 1] || {};
}
- _handleEKey(e: CustomKeyboardEvent) {
- if (this.shouldSuppressKeyboardShortcut(e)) {
+ _handleEKey(e: IronKeyboardEvent) {
+ if (this.shortcuts.shouldSuppress(e)) {
return;
}
@@ -563,14 +564,23 @@
if (isEditing) {
reply.__editing = true;
- }
-
- this.commentsService.addDraft(reply);
-
- if (!isEditing) {
+ this.commentsService.addDraft(reply);
+ } else {
assertIsDefined(this.changeNum, 'changeNum');
assertIsDefined(this.patchNum, 'patchNum');
- this.restApiService.saveDiffDraft(this.changeNum, this.patchNum, reply);
+ this.restApiService
+ .saveDiffDraft(this.changeNum, this.patchNum, reply)
+ .then(result => {
+ if (!result.ok) {
+ fireAlert(document, 'Unable to restore draft');
+ return;
+ }
+ this.restApiService.getResponseObject(result).then(obj => {
+ const resComment = obj as unknown as DraftInfo;
+ resComment.patch_set = reply.patch_set;
+ this.commentsService.addDraft(resComment);
+ });
+ });
}
}
@@ -745,7 +755,8 @@
return -1;
}
- _computeHostClass(unresolved?: boolean) {
+ /** 2nd parameter is for triggering re-computation only. */
+ _computeHostClass(unresolved?: boolean, _?: unknown) {
if (this.isRobotComment) {
return 'robotComment';
}
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 bf9a9dc..c3faaa5 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
@@ -17,6 +17,9 @@
import {html} from '@polymer/polymer/lib/utils/html-tag';
export const htmlTemplate = html`
+ <style include="gr-a11y-styles">
+ /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
+ </style>
<style include="shared-styles">
:host {
font-family: var(--font-family);
@@ -113,6 +116,16 @@
top: 4px;
cursor: pointer;
}
+ .fileName gr-copy-clipboard {
+ display: inline-block;
+ visibility: hidden;
+ vertical-align: top;
+ --gr-button-padding: 0px;
+ }
+ .fileName:focus-within gr-copy-clipboard,
+ .fileName:hover gr-copy-clipboard {
+ visibility: visible;
+ }
</style>
<template is="dom-if" if="[[showFilePath]]">
@@ -127,6 +140,10 @@
>
[[_computeDisplayPath(path)]]
</a>
+ <gr-copy-clipboard
+ hideInput=""
+ text="[[_computeDisplayPath(path)]]"
+ ></gr-copy-clipboard>
</template>
</div>
</template>
@@ -229,7 +246,7 @@
id="diff"
change-num="[[changeNum]]"
diff="[[_diff]]"
- layers="[[_getLayers(_diff)]]"
+ layers="[[layers]]"
path="[[path]]"
prefs="[[_prefs]]"
render-prefs="[[_renderPrefs]]"
@@ -238,9 +255,7 @@
</gr-diff>
<div class="view-diff-container">
<a href="[[_getUrlForViewDiff(comments, changeNum, projectName)]]">
- <gr-button link class="view-diff-button" on-click="_handleViewDiff">
- View Diff
- </gr-button>
+ <gr-button link class="view-diff-button">View Diff</gr-button>
</a>
</div>
</div>
diff --git a/polygerrit-ui/app/elements/shared/gr-comment-thread/gr-comment-thread_test.ts b/polygerrit-ui/app/elements/shared/gr-comment-thread/gr-comment-thread_test.ts
index ecd9731..06d25b3 100644
--- a/polygerrit-ui/app/elements/shared/gr-comment-thread/gr-comment-thread_test.ts
+++ b/polygerrit-ui/app/elements/shared/gr-comment-thread/gr-comment-thread_test.ts
@@ -315,58 +315,64 @@
let element: GrCommentThread;
let addDraftServiceStub: SinonStub;
let saveDiffDraftStub: SinonStub;
+ let comment = {
+ id: '7afa4931_de3d65bd',
+ path: '/path/to/file.txt',
+ line: 5,
+ in_reply_to: 'baf0414d_60047215' as UrlEncodedCommentId,
+ updated: '2015-12-21 02:01:10.850000000',
+ message: 'Done',
+ };
+ const peanutButterComment = {
+ author: {
+ name: 'Mr. Peanutbutter',
+ email: 'tenn1sballchaser@aol.com' as EmailAddress as EmailAddress,
+ },
+ id: 'baf0414d_60047215' as UrlEncodedCommentId,
+ line: 5,
+ in_reply_to: 'baf0414d_60047215' as UrlEncodedCommentId,
+ message: 'is this a crossover episode!?',
+ updated: '2015-12-08 19:48:33.843000000' as Timestamp,
+ path: '/path/to/file.txt',
+ unresolved: true,
+ patch_set: 3 as PatchSetNum,
+ };
+ const mockResponse: Response = {
+ ...new Response(),
+ headers: {} as Headers,
+ redirected: false,
+ status: 200,
+ statusText: '',
+ type: '' as ResponseType,
+ url: '',
+ ok: true,
+ text() {
+ return Promise.resolve(")]}'\n" + JSON.stringify(comment));
+ },
+ };
+ let saveDiffDraftPromiseResolver: (value?: Response) => void;
setup(() => {
addDraftServiceStub = stubComments('addDraft');
stubRestApi('getLoggedIn').returns(Promise.resolve(false));
saveDiffDraftStub = stubRestApi('saveDiffDraft').returns(
- Promise.resolve({
- headers: {} as Headers,
- redirected: false,
- status: 200,
- statusText: '',
- type: '' as ResponseType,
- url: '',
- ok: true,
- text() {
- return Promise.resolve(
- ")]}'\n" +
- JSON.stringify({
- id: '7afa4931_de3d65bd',
- path: '/path/to/file.txt',
- line: 5,
- in_reply_to: 'baf0414d_60047215' as UrlEncodedCommentId,
- updated: '2015-12-21 02:01:10.850000000',
- message: 'Done',
- })
- );
- },
- } as unknown as Response)
+ new Promise<Response>(
+ resolve =>
+ (saveDiffDraftPromiseResolver = resolve as (value?: Response) => void)
+ )
);
stubRestApi('deleteDiffDraft').returns(
- Promise.resolve({ok: true} as unknown as Response)
+ Promise.resolve({...new Response(), ok: true})
);
element = withCommentFixture.instantiate();
element.patchNum = 1 as PatchSetNum;
element.changeNum = 1 as NumericChangeId;
- element.comments = [
- {
- author: {
- name: 'Mr. Peanutbutter',
- email: 'tenn1sballchaser@aol.com' as EmailAddress as EmailAddress,
- },
- id: 'baf0414d_60047215' as UrlEncodedCommentId,
- line: 5,
- message: 'is this a crossover episode!?',
- updated: '2015-12-08 19:48:33.843000000' as Timestamp,
- path: '/path/to/file.txt',
- unresolved: true,
- patch_set: 3 as PatchSetNum,
- },
- ];
+ element.comments = [peanutButterComment];
flush();
});
test('reply', () => {
+ saveDiffDraftPromiseResolver(mockResponse);
+
const commentEl = element.shadowRoot?.querySelector('gr-comment');
const reportStub = stubReporting('recordDraftInteraction');
assert.ok(commentEl);
@@ -385,6 +391,8 @@
});
test('quote reply', () => {
+ saveDiffDraftPromiseResolver(mockResponse);
+
const commentEl = element.shadowRoot?.querySelector('gr-comment');
const reportStub = stubReporting('recordDraftInteraction');
assert.ok(commentEl);
@@ -394,6 +402,10 @@
flush();
const draft = addDraftServiceStub.firstCall.args[0];
+ // the quote reply is not autmatically saved so verify that id is not set
+ assert.isNotOk(draft.id);
+ // verify that the draft returned was not saved
+ assert.isNotOk(saveDiffDraftStub.called);
assert.equal(draft.message, '> is this a crossover episode!?\n\n');
assert.equal(
draft.in_reply_to,
@@ -403,6 +415,7 @@
});
test('quote reply multiline', () => {
+ saveDiffDraftPromiseResolver(mockResponse);
const reportStub = stubReporting('recordDraftInteraction');
element.comments = [
{
@@ -436,6 +449,15 @@
});
test('ack', async () => {
+ saveDiffDraftPromiseResolver(mockResponse);
+ comment = {
+ id: '7afa4931_de3d65bd',
+ path: '/path/to/file.txt',
+ line: 5,
+ in_reply_to: 'baf0414d_60047215' as UrlEncodedCommentId,
+ updated: '2015-12-21 02:01:10.850000000',
+ message: 'Ack',
+ };
const reportStub = stubReporting('recordDraftInteraction');
element.changeNum = 42 as NumericChangeId;
element.patchNum = 1 as PatchSetNum;
@@ -450,11 +472,20 @@
const draft = addDraftServiceStub.firstCall.args[0];
assert.equal(draft.message, 'Ack');
assert.equal(draft.in_reply_to, 'baf0414d_60047215' as UrlEncodedCommentId);
- assert.equal(draft.unresolved, false);
+ assert.isNotOk(draft.unresolved);
assert.isTrue(reportStub.calledOnce);
});
test('done', async () => {
+ saveDiffDraftPromiseResolver(mockResponse);
+ comment = {
+ id: '7afa4931_de3d65bd',
+ path: '/path/to/file.txt',
+ line: 5,
+ in_reply_to: 'baf0414d_60047215' as UrlEncodedCommentId,
+ updated: '2015-12-21 02:01:10.850000000',
+ message: 'Done',
+ };
const reportStub = stubReporting('recordDraftInteraction');
assert.isFalse(saveDiffDraftStub.called);
element.changeNum = 42 as NumericChangeId;
@@ -467,14 +498,18 @@
tap(doneBtn!);
await flush();
const draft = addDraftServiceStub.firstCall.args[0];
+ // Since the reply is automatically saved, verify that draft.id is set in
+ // the model
+ assert.equal(draft.id, '7afa4931_de3d65bd');
assert.equal(draft.message, 'Done');
assert.equal(draft.in_reply_to, 'baf0414d_60047215' as UrlEncodedCommentId);
- assert.isFalse(draft.unresolved);
+ assert.isNotOk(draft.unresolved);
assert.isTrue(reportStub.calledOnce);
assert.isTrue(saveDiffDraftStub.called);
});
test('save', async () => {
+ saveDiffDraftPromiseResolver(mockResponse);
element.changeNum = 42 as NumericChangeId;
element.patchNum = 1 as PatchSetNum;
element.path = '/path/to/file.txt';
@@ -488,13 +523,21 @@
});
test('please fix', async () => {
+ comment = peanutButterComment;
element.changeNum = 42 as NumericChangeId;
element.patchNum = 1 as PatchSetNum;
const commentEl = element.shadowRoot?.querySelector('gr-comment');
assert.ok(commentEl);
const promise = mockPromise();
- commentEl!.addEventListener('create-fix-comment', () => {
- const draft = addDraftServiceStub.firstCall.args[0];
+ commentEl!.addEventListener('create-fix-comment', async () => {
+ assert.isTrue(saveDiffDraftStub.called);
+ assert.isFalse(addDraftServiceStub.called);
+ saveDiffDraftPromiseResolver(mockResponse);
+ // flushing so the saveDiffDraftStub resolves and the draft is returned
+ await flush();
+ assert.isTrue(saveDiffDraftStub.called);
+ assert.isTrue(addDraftServiceStub.called);
+ const draft = saveDiffDraftStub.firstCall.args[2];
assert.equal(
draft.message,
'> is this a crossover episode!?\n\nPlease fix.'
@@ -506,6 +549,8 @@
assert.isTrue(draft.unresolved);
promise.resolve();
});
+ assert.isFalse(saveDiffDraftStub.called);
+ assert.isFalse(addDraftServiceStub.called);
commentEl!.dispatchEvent(
new CustomEvent('create-fix-comment', {
detail: {comment: commentEl!.comment},
@@ -839,6 +884,7 @@
stubRestApi('getLoggedIn').returns(Promise.resolve(false));
stubRestApi('saveDiffDraft').returns(
Promise.resolve({
+ ...new Response(),
ok: true,
text() {
return Promise.resolve(
@@ -853,10 +899,10 @@
})
);
},
- } as unknown as Response)
+ })
);
stubRestApi('deleteDiffDraft').returns(
- Promise.resolve({ok: true} as unknown as Response)
+ Promise.resolve({...new Response(), ok: true})
);
element = withCommentFixture.instantiate();
element.patchNum = 1 as PatchSetNum;
diff --git a/polygerrit-ui/app/elements/shared/gr-comment/gr-comment.ts b/polygerrit-ui/app/elements/shared/gr-comment/gr-comment.ts
index 418cd0e..0d4ad8c 100644
--- a/polygerrit-ui/app/elements/shared/gr-comment/gr-comment.ts
+++ b/polygerrit-ui/app/elements/shared/gr-comment/gr-comment.ts
@@ -20,7 +20,6 @@
import '../../plugins/gr-endpoint-param/gr-endpoint-param';
import '../gr-button/gr-button';
import '../gr-dialog/gr-dialog';
-import '../gr-date-formatter/gr-date-formatter';
import '../gr-formatted-text/gr-formatted-text';
import '../gr-icons/gr-icons';
import '../gr-overlay/gr-overlay';
@@ -50,6 +49,7 @@
import {GrConfirmDeleteCommentDialog} from '../gr-confirm-delete-comment-dialog/gr-confirm-delete-comment-dialog';
import {
isDraft,
+ isRobot,
UIComment,
UIDraft,
UIRobot,
@@ -215,7 +215,7 @@
projectConfig?: ConfigInfo;
@property({type: Boolean})
- robotButtonDisabled?: boolean;
+ robotButtonDisabled = false;
@property({type: Boolean})
_hasHumanReply?: boolean;
@@ -234,7 +234,7 @@
side?: string;
@property({type: Boolean})
- resolved?: boolean;
+ resolved = false;
// Intentional to share the object across instances.
@property({type: Object})
@@ -319,12 +319,13 @@
super.disconnectedCallback();
}
- _getAuthor(comment: UIComment) {
- return comment.author || this._selfAccount;
+ /** 2nd argument is for *triggering* the computation only. */
+ _getAuthor(comment?: UIComment, _?: unknown) {
+ return comment?.author || this._selfAccount;
}
- _getUrlForComment(comment: UIComment) {
- if (!this.changeNum || !this.projectName) return '';
+ _getUrlForComment(comment?: UIComment) {
+ if (!comment || !this.changeNum || !this.projectName) return '';
if (!comment.id) throw new Error('comment must have an id');
return GerritNav.getUrlForComment(
this.changeNum as NumericChangeId,
@@ -436,8 +437,8 @@
this._showRobotActions = showActions && isRobotComment;
}
- hasPublishedComment(comments: UIComment[]) {
- if (!comments.length) return false;
+ hasPublishedComment(comments?: UIComment[]) {
+ if (!comments?.length) return false;
return comments.length > 1 || !isDraft(comments[0]);
}
@@ -675,7 +676,13 @@
@observe('comment.message')
_commentMessageChanged(message: string) {
- this._messageText = message || '';
+ /*
+ * Only overwrite the message text user has typed if there is no existing
+ * text typed by the user. This prevents the bug where creating another
+ * comment triggered a recomputation of comments and the text written by
+ * the user was lost.
+ */
+ if (!this._messageText) this._messageText = message || '';
}
_messageTextChanged(_: string, oldValue: string) {
@@ -803,7 +810,7 @@
);
}
- _hasNoFix(comment: UIComment) {
+ _hasNoFix(comment?: UIComment) {
return !comment || !(comment as UIRobot).fix_suggestions;
}
@@ -1032,9 +1039,10 @@
return overlay.open();
}
- _computeHideRunDetails(comment: UIRobot, collapsed: boolean) {
+ _computeHideRunDetails(comment: UIComment | undefined, collapsed: boolean) {
if (!comment) return true;
- return !(comment.robot_id && comment.url && !collapsed);
+ if (!isRobot(comment)) return true;
+ return !comment.url || collapsed;
}
_closeOverlay(overlay?: GrOverlay | null) {
diff --git a/polygerrit-ui/app/elements/shared/gr-comment/gr-comment_html.ts b/polygerrit-ui/app/elements/shared/gr-comment/gr-comment_html.ts
index b00bf8b..b77c4b2 100644
--- a/polygerrit-ui/app/elements/shared/gr-comment/gr-comment_html.ts
+++ b/polygerrit-ui/app/elements/shared/gr-comment/gr-comment_html.ts
@@ -89,10 +89,7 @@
justify-content: flex-end;
}
.rightActions gr-button {
- --gr-button: {
- height: 20px;
- padding: 0 var(--spacing-s);
- }
+ --gr-button-padding: 0 var(--spacing-s);
}
.editMessage {
display: none;
@@ -190,10 +187,8 @@
}
#deleteBtn {
display: none;
- --gr-button: {
- color: var(--deemphasized-text-color);
- padding: 0;
- }
+ --gr-button-text-color: var(--deemphasized-text-color);
+ --gr-button-padding: 0;
}
#deleteBtn.showDeleteButtons {
display: block;
@@ -275,10 +270,10 @@
</template>
<gr-tooltip-content
class="draftTooltip"
- has-tooltip=""
+ has-tooltip
title="[[_computeDraftTooltip(_unableToSave)]]"
max-width="20em"
- show-icon=""
+ show-icon
>
<span class="draftLabel">[[_computeDraftText(_unableToSave)]]</span>
</gr-tooltip-content>
@@ -313,7 +308,7 @@
<template is="dom-if" if="[[comment.updated]]">
<span class="date" tabindex="0" on-click="_handleAnchorClick">
<gr-date-formatter
- has-tooltip=""
+ withTooltip
date-str="[[comment.updated]]"
></gr-date-formatter>
</span>
@@ -321,7 +316,7 @@
<div class="show-hide" tabindex="0">
<label
class="show-hide"
- aria-label="[[_computeShowHideAriaLabel(collapsed)]]"
+ aria-label$="[[_computeShowHideAriaLabel(collapsed)]]"
>
<input
type="checkbox"
@@ -357,7 +352,7 @@
<div class="respectfulReviewTip">
<div>
<gr-tooltip-content
- has-tooltip=""
+ has-tooltip
title="Tips for respectful code reviews."
>
<iron-icon
diff --git a/polygerrit-ui/app/elements/shared/gr-comment/gr-comment_test.ts b/polygerrit-ui/app/elements/shared/gr-comment/gr-comment_test.ts
index 31a1614..b963d4b 100644
--- a/polygerrit-ui/app/elements/shared/gr-comment/gr-comment_test.ts
+++ b/polygerrit-ui/app/elements/shared/gr-comment/gr-comment_test.ts
@@ -217,6 +217,29 @@
assert.isTrue(storageStub.called);
});
+ test('comment message sets messageText only when empty', () => {
+ element.changeNum = 1 as NumericChangeId;
+ element.patchNum = 1 as PatchSetNum;
+ element._messageText = '';
+ element.comment = {
+ author: {
+ name: 'Mr. Peanutbutter',
+ email: 'tenn1sballchaser@aol.com' as EmailAddress,
+ },
+ line: 5,
+ path: 'test',
+ __editing: true,
+ __draft: true,
+ message: 'hello world',
+ };
+ // messageText was empty so overwrite the message now
+ assert.equal(element._messageText, 'hello world');
+
+ element.comment!.message = 'new message';
+ // messageText was already set so do not overwrite it
+ assert.equal(element._messageText, 'hello world');
+ });
+
test('_getPatchNum', () => {
element.side = 'PARENT';
element.patchNum = 1 as PatchSetNum;
diff --git a/polygerrit-ui/app/elements/shared/gr-copy-clipboard/gr-copy-clipboard.ts b/polygerrit-ui/app/elements/shared/gr-copy-clipboard/gr-copy-clipboard.ts
index 2e95bd0..01422a9 100644
--- a/polygerrit-ui/app/elements/shared/gr-copy-clipboard/gr-copy-clipboard.ts
+++ b/polygerrit-ui/app/elements/shared/gr-copy-clipboard/gr-copy-clipboard.ts
@@ -81,35 +81,24 @@
iron-icon {
color: var(--deemphasized-text-color);
vertical-align: top;
+ --iron-icon-height: 20px;
+ --iron-icon-width: 20px;
+ }
+ gr-button {
+ display: block;
+ --gr-button-padding: 2px;
}
`,
];
}
override render() {
- // To pass CSS mixins for @apply to Polymer components, they need to appear
- // in <style> inside the template.
- const customStyle = html`
- <style>
- iron-icon {
- --iron-icon-height: 20px;
- --iron-icon-width: 20px;
- }
- gr-button {
- --gr-button: {
- padding: 2px;
- }
- }
- </style>
- `;
- return html`${customStyle}
+ return html`
<div class="text">
<iron-input
class="copyText"
- type="text"
@click="${this._handleInputClick}"
- readonly=""
- bind-value=${this.text}
+ .bindValue=${this.text ?? ''}
>
<input
id="input"
@@ -118,22 +107,26 @@
type="text"
@click="${this._handleInputClick}"
readonly=""
- .value=${this.text}
+ .value=${this.text ?? ''}
part="text-container-style"
/>
</iron-input>
- <gr-button
- id="copy-clipboard-button"
- link=""
+ <gr-tooltip-content
?has-tooltip=${this.hasTooltip}
- class="copyToClipboard"
title="${ifDefined(this.buttonTitle)}"
- @click="${this._copyToClipboard}"
- aria-label="Click to copy to clipboard"
>
- <iron-icon id="icon" icon="gr-icons:content-copy"></iron-icon>
- </gr-button>
- </div> `;
+ <gr-button
+ id="copy-clipboard-button"
+ link=""
+ class="copyToClipboard"
+ @click="${this._copyToClipboard}"
+ aria-label="Click to copy to clipboard"
+ >
+ <iron-icon id="icon" icon="gr-icons:content-copy"></iron-icon>
+ </gr-button>
+ </gr-tooltip-content>
+ </div>
+ `;
}
focusOnCopy() {
diff --git a/polygerrit-ui/app/elements/shared/gr-cursor-manager/gr-cursor-manager.ts b/polygerrit-ui/app/elements/shared/gr-cursor-manager/gr-cursor-manager.ts
index 9dce127..9f65dd4 100644
--- a/polygerrit-ui/app/elements/shared/gr-cursor-manager/gr-cursor-manager.ts
+++ b/polygerrit-ui/app/elements/shared/gr-cursor-manager/gr-cursor-manager.ts
@@ -179,9 +179,6 @@
private async getVisibleEntries(
filter?: (el: Element) => boolean
): Promise<IntersectionObserverEntry[]> {
- if (!this._isIntersectionObserverSupported()) {
- throw new Error('Intersection observing not supported');
- }
if (!this.stops) {
return [];
}
@@ -218,14 +215,6 @@
});
}
- _isIntersectionObserverSupported() {
- // The copy of this method exists in gr-app-element.js under the
- // name _isCursorManagerSupportMoveToVisibleLine
- // If you update this method, you must update gr-app-element.js
- // as well.
- return 'IntersectionObserver' in window;
- }
-
/**
* Set the cursor to an arbitrary stop - if the given element is not one of
* the stops, unset the cursor.
diff --git a/polygerrit-ui/app/elements/shared/gr-date-formatter/gr-date-formatter.ts b/polygerrit-ui/app/elements/shared/gr-date-formatter/gr-date-formatter.ts
index a3800a8..99f9265 100644
--- a/polygerrit-ui/app/elements/shared/gr-date-formatter/gr-date-formatter.ts
+++ b/polygerrit-ui/app/elements/shared/gr-date-formatter/gr-date-formatter.ts
@@ -14,11 +14,9 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-import '../../../styles/shared-styles';
-import {PolymerElement} from '@polymer/polymer/polymer-element';
-import {htmlTemplate} from './gr-date-formatter_html';
-import {TooltipMixin} from '../../../mixins/gr-tooltip-mixin/gr-tooltip-mixin';
-import {property, customElement} from '@polymer/decorators';
+import '../gr-tooltip-content/gr-tooltip-content';
+import {css, html, LitElement} from 'lit';
+import {customElement, property} from 'lit/decorators';
import {
parseDate,
fromNow,
@@ -75,17 +73,10 @@
}
}
-// This avoids JSC_DYNAMIC_EXTENDS_WITHOUT_JSDOC closure compiler error.
-const base = TooltipMixin(PolymerElement);
-
@customElement('gr-date-formatter')
-export class GrDateFormatter extends base {
- static get template() {
- return htmlTemplate;
- }
-
- @property({type: String, notify: true})
- dateStr: string | null = null;
+export class GrDateFormatter extends LitElement {
+ @property({type: String})
+ dateStr: string | undefined = undefined;
@property({type: Boolean})
showDateAndTime = false;
@@ -95,30 +86,20 @@
* native browser tooltip.
*/
@property({type: Boolean})
- override hasTooltip = false;
+ withTooltip = false;
@property({type: Boolean})
showYesterday = false;
- /**
- * The title to be used as the native tooltip or by the tooltip behavior.
- */
- @property({
- type: String,
- reflectToAttribute: true,
- computed: '_computeFullDateStr(dateStr, _timeFormat, _dateFormat)',
- })
- override title = '';
-
/** @type {?{short: string, full: string}} */
@property({type: Object})
- _dateFormat?: DateFormatPair;
+ private dateFormat?: DateFormatPair;
@property({type: String})
- _timeFormat?: string;
+ private timeFormat?: string;
@property({type: Boolean})
- _relative = false;
+ private relative = false;
@property({type: Boolean})
forceRelative = false;
@@ -132,76 +113,110 @@
super();
}
+ static override get styles() {
+ return [
+ css`
+ host {
+ color: inherit;
+ display: inline;
+ }
+ `,
+ ];
+ }
+
+ override render() {
+ if (!this.withTooltip) {
+ return this.renderDateString();
+ }
+
+ const fullDateStr = this.computeFullDateStr();
+ if (!fullDateStr) {
+ return this.renderDateString();
+ }
+ return html`
+ <gr-tooltip-content has-tooltip title=${fullDateStr}>
+ ${this.renderDateString()}
+ </gr-tooltip-content>
+ `;
+ }
+
+ private renderDateString() {
+ return html` <span>${this._computeDateStr()}</span>`;
+ }
+
override connectedCallback() {
super.connectedCallback();
this._loadPreferences();
}
+ // private but used by tests
_getUtcOffsetString() {
return utcOffsetString();
}
+ // private but used by tests
_loadPreferences() {
return this._getLoggedIn().then(loggedIn => {
if (!loggedIn) {
- this._timeFormat = TimeFormats.TIME_24;
- this._dateFormat = DateFormats.STD;
- this._relative = this.forceRelative;
+ this.timeFormat = TimeFormats.TIME_24;
+ this.dateFormat = DateFormats.STD;
+ this.relative = this.forceRelative;
return;
}
- return Promise.all([this._loadTimeFormat(), this._loadRelative()]);
+ return Promise.all([this._loadTimeFormat(), this.loadRelative()]);
});
}
+ // private but used in gr/file-list_test.js
_loadTimeFormat() {
- return this._getPreferences().then(preferences => {
+ return this.getPreferences().then(preferences => {
if (!preferences) {
throw Error('Preferences is not set');
}
- this._decideTimeFormat(preferences.time_format);
- this._decideDateFormat(preferences.date_format);
+ this.decideTimeFormat(preferences.time_format);
+ this.decideDateFormat(preferences.date_format);
});
}
- _decideTimeFormat(timeFormat: TimeFormat) {
+ private decideTimeFormat(timeFormat: TimeFormat) {
switch (timeFormat) {
case TimeFormat.HHMM_12:
- this._timeFormat = TimeFormats.TIME_12;
+ this.timeFormat = TimeFormats.TIME_12;
break;
case TimeFormat.HHMM_24:
- this._timeFormat = TimeFormats.TIME_24;
+ this.timeFormat = TimeFormats.TIME_24;
break;
default:
assertNever(timeFormat, `Invalid time format: ${timeFormat}`);
}
}
- _decideDateFormat(dateFormat: DateFormat) {
+ private decideDateFormat(dateFormat: DateFormat) {
switch (dateFormat) {
case DateFormat.STD:
- this._dateFormat = DateFormats.STD;
+ this.dateFormat = DateFormats.STD;
break;
case DateFormat.US:
- this._dateFormat = DateFormats.US;
+ this.dateFormat = DateFormats.US;
break;
case DateFormat.ISO:
- this._dateFormat = DateFormats.ISO;
+ this.dateFormat = DateFormats.ISO;
break;
case DateFormat.EURO:
- this._dateFormat = DateFormats.EURO;
+ this.dateFormat = DateFormats.EURO;
break;
case DateFormat.UK:
- this._dateFormat = DateFormats.UK;
+ this.dateFormat = DateFormats.UK;
break;
default:
assertNever(dateFormat, `Invalid date format: ${dateFormat}`);
}
}
- _loadRelative() {
- return this._getPreferences().then(prefs => {
+ private loadRelative() {
+ return this.getPreferences().then(prefs => {
// prefs.relative_date_in_change_table is not set when false.
- this._relative =
+ this.relative =
this.forceRelative || !!(prefs && prefs.relative_date_in_change_table);
});
}
@@ -210,70 +225,60 @@
return this.restApiService.getLoggedIn();
}
- _getPreferences() {
+ private getPreferences() {
return this.restApiService.getPreferences();
}
- _computeDateStr(
- dateStr?: Timestamp,
- timeFormat?: string,
- dateFormat?: DateFormatPair,
- relative?: boolean,
- showDateAndTime?: boolean,
- showYesterday?: boolean
- ) {
- if (!dateStr || !timeFormat || !dateFormat) {
+ // private but used by tests
+ _computeDateStr() {
+ if (!this.dateStr || !this.timeFormat || !this.dateFormat) {
return '';
}
- const date = parseDate(dateStr);
+ const date = parseDate(this.dateStr as Timestamp);
if (!isValidDate(date)) {
return '';
}
- if (relative) {
+ if (this.relative) {
return fromNow(date, this.relativeOptionNoAgo);
}
const now = new Date();
- let format = dateFormat.full;
+ let format = this.dateFormat.full;
if (isWithinDay(now, date)) {
- format = timeFormat;
- } else if (showYesterday && wasYesterday(now, date)) {
- return `Yesterday at ${formatDate(date, timeFormat)}`;
+ format = this.timeFormat;
+ } else if (this.showYesterday && wasYesterday(now, date)) {
+ return `Yesterday at ${formatDate(date, this.timeFormat)}`;
} else {
if (isWithinHalfYear(now, date)) {
- format = dateFormat.short;
+ format = this.dateFormat.short;
}
- if (this.showDateAndTime || showDateAndTime) {
- format = `${format} ${timeFormat}`;
+ if (this.showDateAndTime || this.showDateAndTime) {
+ format = `${format} ${this.timeFormat}`;
}
}
return formatDate(date, format);
}
- _timeToSecondsFormat(timeFormat: string | undefined) {
- return timeFormat === TimeFormats.TIME_12
- ? TimeFormats.TIME_12_WITH_SEC
- : TimeFormats.TIME_24_WITH_SEC;
- }
-
- _computeFullDateStr(
- dateStr?: Timestamp,
- timeFormat?: string,
- dateFormat?: DateFormatPair
- ) {
+ private computeFullDateStr() {
// Polymer 2: check for undefined
- if ([dateStr, timeFormat].includes(undefined) || !dateFormat) {
+ if (
+ [this.dateStr, this.timeFormat].includes(undefined) ||
+ !this.dateFormat
+ ) {
return undefined;
}
- if (!dateStr) {
+ if (!this.dateStr) {
return '';
}
- const date = parseDate(dateStr);
+ const date = parseDate(this.dateStr as Timestamp);
if (!isValidDate(date)) {
return '';
}
- let format = dateFormat.full + ', ';
- format += this._timeToSecondsFormat(timeFormat);
+ let format = this.dateFormat.full + ', ';
+ format +=
+ this.timeFormat === TimeFormats.TIME_12
+ ? TimeFormats.TIME_12_WITH_SEC
+ : TimeFormats.TIME_24_WITH_SEC;
return formatDate(date, format) + this._getUtcOffsetString();
}
}
diff --git a/polygerrit-ui/app/elements/shared/gr-date-formatter/gr-date-formatter_html.ts b/polygerrit-ui/app/elements/shared/gr-date-formatter/gr-date-formatter_html.ts
deleted file mode 100644
index 4808832..0000000
--- a/polygerrit-ui/app/elements/shared/gr-date-formatter/gr-date-formatter_html.ts
+++ /dev/null
@@ -1,30 +0,0 @@
-/**
- * @license
- * Copyright (C) 2020 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 {html} from '@polymer/polymer/lib/utils/html-tag';
-
-export const htmlTemplate = html`
- <style>
- :host {
- color: inherit;
- display: inline;
- }
- </style>
- <span>
- [[_computeDateStr(dateStr, _timeFormat, _dateFormat, _relative,
- showDateAndTime, showYesterday)]]
- </span>
-`;
diff --git a/polygerrit-ui/app/elements/shared/gr-date-formatter/gr-date-formatter_test.js b/polygerrit-ui/app/elements/shared/gr-date-formatter/gr-date-formatter_test.js
index 9a96c2d..860a7e7 100644
--- a/polygerrit-ui/app/elements/shared/gr-date-formatter/gr-date-formatter_test.js
+++ b/polygerrit-ui/app/elements/shared/gr-date-formatter/gr-date-formatter_test.js
@@ -22,14 +22,18 @@
import {stubRestApi} from '../../../test/test-utils.js';
const basicFixture = fixtureFromTemplate(html`
-<gr-date-formatter date-str="2015-09-24 23:30:17.033000000"></gr-date-formatter>
+<gr-date-formatter withTooltip dateStr="2015-09-24 23:30:17.033000000">
+</gr-date-formatter>
+`);
+
+const lightFixture = fixtureFromTemplate(html`
+<gr-date-formatter dateStr="2015-09-24 23:30:17.033000000"></gr-date-formatter>
`);
suite('gr-date-formatter tests', () => {
let element;
setup(() => {
-
});
/**
@@ -41,7 +45,7 @@
return d;
}
- function testDates(nowStr, dateStr, expected, expectedWithDateAndTime,
+ async function testDates(nowStr, dateStr, expected, expectedWithDateAndTime,
expectedTooltip) {
// Normalize and convert the date to mimic server response.
dateStr = normalizedDate(dateStr)
@@ -50,13 +54,13 @@
.slice(0, -1);
sinon.useFakeTimers(normalizedDate(nowStr).getTime());
element.dateStr = dateStr;
- flush();
- const span = element.shadowRoot
- .querySelector('span');
+ await element.updateComplete;
+ const span = element.shadowRoot.querySelector('span');
+ const tooltip = element.shadowRoot.querySelector('gr-tooltip-content');
assert.equal(span.textContent.trim(), expected);
- assert.equal(element.title, expectedTooltip);
+ assert.equal(tooltip.title, expectedTooltip);
element.showDateAndTime = true;
- flush();
+ await element.updateComplete;
assert.equal(span.textContent.trim(), expectedWithDateAndTime);
}
@@ -81,35 +85,37 @@
test('invalid dates are quietly rejected', () => {
assert.notOk((new Date('foo')).valueOf());
- assert.equal(element._computeDateStr('foo', 'h:mm A'), '');
+ element.dateStr = 'foo';
+ element.timeFormat = 'h:mm A';
+ assert.equal(element._computeDateStr(), '');
});
- test('Within 24 hours on same day', () => {
- testDates('2015-07-29 20:34:14.985000000',
+ test('Within 24 hours on same day', async () => {
+ await testDates('2015-07-29 20:34:14.985000000',
'2015-07-29 15:34:14.985000000',
'15:34',
'15:34',
'Jul 29, 2015, 15:34:14');
});
- test('Within 24 hours on different days', () => {
- testDates('2015-07-29 03:34:14.985000000',
+ test('Within 24 hours on different days', async () => {
+ await testDates('2015-07-29 03:34:14.985000000',
'2015-07-28 20:25:14.985000000',
'Jul 28',
'Jul 28 20:25',
'Jul 28, 2015, 20:25:14');
});
- test('More than 24 hours but less than six months', () => {
- testDates('2015-07-29 20:34:14.985000000',
+ test('More than 24 hours but less than six months', async () => {
+ await testDates('2015-07-29 20:34:14.985000000',
'2015-06-15 03:25:14.985000000',
'Jun 15',
'Jun 15 03:25',
'Jun 15, 2015, 03:25:14');
});
- test('More than six months', () => {
- testDates('2015-09-15 20:34:00.000000000',
+ test('More than six months', async () => {
+ await testDates('2015-09-15 20:34:00.000000000',
'2015-01-15 03:25:00.000000000',
'Jan 15, 2015',
'Jan 15, 2015 03:25',
@@ -128,24 +134,24 @@
return element._loadPreferences();
}));
- test('Within 24 hours on same day', () => {
- testDates('2015-07-29 20:34:14.985000000',
+ test('Within 24 hours on same day', async () => {
+ await testDates('2015-07-29 20:34:14.985000000',
'2015-07-29 15:34:14.985000000',
'15:34',
'15:34',
'07/29/15, 15:34:14');
});
- test('Within 24 hours on different days', () => {
- testDates('2015-07-29 03:34:14.985000000',
+ test('Within 24 hours on different days', async () => {
+ await testDates('2015-07-29 03:34:14.985000000',
'2015-07-28 20:25:14.985000000',
'07/28',
'07/28 20:25',
'07/28/15, 20:25:14');
});
- test('More than 24 hours but less than six months', () => {
- testDates('2015-07-29 20:34:14.985000000',
+ test('More than 24 hours but less than six months', async () => {
+ await testDates('2015-07-29 20:34:14.985000000',
'2015-06-15 03:25:14.985000000',
'06/15',
'06/15 03:25',
@@ -164,24 +170,24 @@
return element._loadPreferences();
}));
- test('Within 24 hours on same day', () => {
- testDates('2015-07-29 20:34:14.985000000',
+ test('Within 24 hours on same day', async () => {
+ await testDates('2015-07-29 20:34:14.985000000',
'2015-07-29 15:34:14.985000000',
'15:34',
'15:34',
'2015-07-29, 15:34:14');
});
- test('Within 24 hours on different days', () => {
- testDates('2015-07-29 03:34:14.985000000',
+ test('Within 24 hours on different days', async () => {
+ await testDates('2015-07-29 03:34:14.985000000',
'2015-07-28 20:25:14.985000000',
'07-28',
'07-28 20:25',
'2015-07-28, 20:25:14');
});
- test('More than 24 hours but less than six months', () => {
- testDates('2015-07-29 20:34:14.985000000',
+ test('More than 24 hours but less than six months', async () => {
+ await testDates('2015-07-29 20:34:14.985000000',
'2015-06-15 03:25:14.985000000',
'06-15',
'06-15 03:25',
@@ -200,24 +206,24 @@
return element._loadPreferences();
}));
- test('Within 24 hours on same day', () => {
- testDates('2015-07-29 20:34:14.985000000',
+ test('Within 24 hours on same day', async () => {
+ await testDates('2015-07-29 20:34:14.985000000',
'2015-07-29 15:34:14.985000000',
'15:34',
'15:34',
'29.07.2015, 15:34:14');
});
- test('Within 24 hours on different days', () => {
- testDates('2015-07-29 03:34:14.985000000',
+ test('Within 24 hours on different days', async () => {
+ await testDates('2015-07-29 03:34:14.985000000',
'2015-07-28 20:25:14.985000000',
'28. Jul',
'28. Jul 20:25',
'28.07.2015, 20:25:14');
});
- test('More than 24 hours but less than six months', () => {
- testDates('2015-07-29 20:34:14.985000000',
+ test('More than 24 hours but less than six months', async () => {
+ await testDates('2015-07-29 20:34:14.985000000',
'2015-06-15 03:25:14.985000000',
'15. Jun',
'15. Jun 03:25',
@@ -236,24 +242,24 @@
return element._loadPreferences();
}));
- test('Within 24 hours on same day', () => {
- testDates('2015-07-29 20:34:14.985000000',
+ test('Within 24 hours on same day', async () => {
+ await testDates('2015-07-29 20:34:14.985000000',
'2015-07-29 15:34:14.985000000',
'15:34',
'15:34',
'29/07/2015, 15:34:14');
});
- test('Within 24 hours on different days', () => {
- testDates('2015-07-29 03:34:14.985000000',
+ test('Within 24 hours on different days', async () => {
+ await testDates('2015-07-29 03:34:14.985000000',
'2015-07-28 20:25:14.985000000',
'28/07',
'28/07 20:25',
'28/07/2015, 20:25:14');
});
- test('More than 24 hours but less than six months', () => {
- testDates('2015-07-29 20:34:14.985000000',
+ test('More than 24 hours but less than six months', async () => {
+ await testDates('2015-07-29 20:34:14.985000000',
'2015-06-15 03:25:14.985000000',
'15/06',
'15/06 03:25',
@@ -273,8 +279,8 @@
})
);
- test('Within 24 hours on same day', () => {
- testDates('2015-07-29 20:34:14.985000000',
+ test('Within 24 hours on same day', async () => {
+ await testDates('2015-07-29 20:34:14.985000000',
'2015-07-29 15:34:14.985000000',
'3:34 PM',
'3:34 PM',
@@ -294,8 +300,8 @@
})
);
- test('Within 24 hours on same day', () => {
- testDates('2015-07-29 20:34:14.985000000',
+ test('Within 24 hours on same day', async () => {
+ await testDates('2015-07-29 20:34:14.985000000',
'2015-07-29 15:34:14.985000000',
'3:34 PM',
'3:34 PM',
@@ -315,8 +321,8 @@
})
);
- test('Within 24 hours on same day', () => {
- testDates('2015-07-29 20:34:14.985000000',
+ test('Within 24 hours on same day', async () => {
+ await testDates('2015-07-29 20:34:14.985000000',
'2015-07-29 15:34:14.985000000',
'3:34 PM',
'3:34 PM',
@@ -336,8 +342,8 @@
})
);
- test('Within 24 hours on same day', () => {
- testDates('2015-07-29 20:34:14.985000000',
+ test('Within 24 hours on same day', async () => {
+ await testDates('2015-07-29 20:34:14.985000000',
'2015-07-29 15:34:14.985000000',
'3:34 PM',
'3:34 PM',
@@ -357,8 +363,8 @@
})
);
- test('Within 24 hours on same day', () => {
- testDates('2015-07-29 20:34:14.985000000',
+ test('Within 24 hours on same day', async () => {
+ await testDates('2015-07-29 20:34:14.985000000',
'2015-07-29 15:34:14.985000000',
'3:34 PM',
'3:34 PM',
@@ -377,16 +383,16 @@
return element._loadPreferences();
}));
- test('Within 24 hours on same day', () => {
- testDates('2015-07-29 20:34:14.985000000',
+ test('Within 24 hours on same day', async () => {
+ await testDates('2015-07-29 20:34:14.985000000',
'2015-07-29 15:34:14.985000000',
'5 hours ago',
'5 hours ago',
'Jul 29, 2015, 3:34:14 PM');
});
- test('More than six months', () => {
- testDates('2015-09-15 20:34:00.000000000',
+ test('More than six months', async () => {
+ await testDates('2015-09-15 20:34:00.000000000',
'2015-01-15 03:25:00.000000000',
'8 months ago',
'8 months ago',
@@ -405,10 +411,10 @@
}));
test('Preferences are respected', () => {
- assert.equal(element._timeFormat, 'h:mm A');
- assert.equal(element._dateFormat.short, 'MM/DD');
- assert.equal(element._dateFormat.full, 'MM/DD/YY');
- assert.isTrue(element._relative);
+ assert.equal(element.timeFormat, 'h:mm A');
+ assert.equal(element.dateFormat.short, 'MM/DD');
+ assert.equal(element.dateFormat.full, 'MM/DD/YY');
+ assert.isTrue(element.relative);
});
});
@@ -419,10 +425,38 @@
}));
test('Default preferences are respected', () => {
- assert.equal(element._timeFormat, 'HH:mm');
- assert.equal(element._dateFormat.short, 'MMM DD');
- assert.equal(element._dateFormat.full, 'MMM DD, YYYY');
- assert.isFalse(element._relative);
+ assert.equal(element.timeFormat, 'HH:mm');
+ assert.equal(element.dateFormat.short, 'MMM DD');
+ assert.equal(element.dateFormat.full, 'MMM DD, YYYY');
+ assert.isFalse(element.relative);
+ });
+ });
+
+ suite('with tooltip', () => {
+ setup(async () => {
+ await stubRestAPI(null);
+ element = basicFixture.instantiate();
+ await element._loadPreferences();
+ await element.updateComplete;
+ });
+
+ test('Tooltip is present', () => {
+ const tooltip = element.shadowRoot.querySelector('gr-tooltip-content');
+ assert.isOk(tooltip);
+ });
+ });
+
+ suite('without tooltip', () => {
+ setup(async () => {
+ await stubRestAPI(null);
+ element = lightFixture.instantiate();
+ await element._loadPreferences();
+ await element.updateComplete;
+ });
+
+ test('Tooltip is absent', () => {
+ const tooltip = element.shadowRoot.querySelector('gr-tooltip-content');
+ assert.isNotOk(tooltip);
});
});
});
diff --git a/polygerrit-ui/app/elements/shared/gr-dialog/gr-dialog.ts b/polygerrit-ui/app/elements/shared/gr-dialog/gr-dialog.ts
index 27c6341..97ee39e 100644
--- a/polygerrit-ui/app/elements/shared/gr-dialog/gr-dialog.ts
+++ b/polygerrit-ui/app/elements/shared/gr-dialog/gr-dialog.ts
@@ -15,13 +15,11 @@
* limitations under the License.
*/
import '../gr-button/gr-button';
-import '../../../styles/gr-font-styles';
-import '../../../styles/shared-styles';
-import {PolymerElement} from '@polymer/polymer/polymer-element';
-import {htmlTemplate} from './gr-dialog_html';
-import {customElement, property, observe} from '@polymer/decorators';
+import {customElement, property, query} from 'lit/decorators';
import {GrButton} from '../gr-button/gr-button';
-import {KeydownEvent} from '../../../types/events';
+import {css, html, LitElement, PropertyValues} from 'lit';
+import {sharedStyles} from '../../../styles/shared-styles';
+import {fontStyles} from '../../../styles/gr-font-styles';
declare global {
interface HTMLElementTagNameMap {
@@ -29,18 +27,8 @@
}
}
-export interface GrDialog {
- $: {
- confirm: GrButton;
- };
-}
-
@customElement('gr-dialog')
-export class GrDialog extends PolymerElement {
- static get template() {
- return htmlTemplate;
- }
-
+export class GrDialog extends LitElement {
/**
* Fired when the confirm button is pressed.
*
@@ -53,33 +41,132 @@
* @event cancel
*/
- @property({type: String})
+ @query('#confirm')
+ confirmButton?: GrButton;
+
+ @property({type: String, attribute: 'confirm-label'})
confirmLabel = 'Confirm';
// Supplying an empty cancel label will hide the button completely.
- @property({type: String})
+ @property({type: String, attribute: 'cancel-label'})
cancelLabel = 'Cancel';
@property({type: Boolean})
disabled = false;
- @property({type: Boolean})
+ @property({type: Boolean, attribute: 'confirm-on-enter'})
confirmOnEnter = false;
- @property({type: String})
+ @property({type: String, attribute: 'confirm-tooltip'})
confirmTooltip?: string;
- override ready() {
- super.ready();
- this._ensureAttribute('role', 'dialog');
+ override firstUpdated(changedProperties: PropertyValues) {
+ super.firstUpdated(changedProperties);
+ if (!this.getAttribute('role')) this.setAttribute('role', 'dialog');
}
- @observe('confirmTooltip')
- _handleConfirmTooltipUpdate(confirmTooltip?: string) {
- if (confirmTooltip) {
- this.$.confirm.setAttribute('has-tooltip', 'true');
+ static override get styles() {
+ return [
+ sharedStyles,
+ fontStyles,
+ css`
+ :host {
+ color: var(--primary-text-color);
+ display: block;
+ max-height: 90vh;
+ overflow: auto;
+ }
+ .container {
+ display: flex;
+ flex-direction: column;
+ max-height: 90vh;
+ padding: var(--spacing-xl);
+ }
+ header {
+ flex-shrink: 0;
+ padding-bottom: var(--spacing-xl);
+ }
+ main {
+ display: flex;
+ flex-shrink: 1;
+ width: 100%;
+ flex: 1;
+ /* IMPORTANT: required for firefox */
+ min-height: 0px;
+ }
+ main .overflow-container {
+ flex: 1;
+ overflow: auto;
+ }
+ footer {
+ display: flex;
+ flex-shrink: 0;
+ justify-content: flex-end;
+ padding-top: var(--spacing-xl);
+ }
+ gr-button {
+ margin-left: var(--spacing-l);
+ }
+ .hidden {
+ display: none;
+ }
+ `,
+ ];
+ }
+
+ override render() {
+ // Note that we are using (e: Event) => this._handleKeyDown because the
+ // tests mock out _handleKeydown so the lookup needs to be dynamic, not
+ // bound statically here.
+ return html`
+ <div
+ class="container"
+ @keydown=${(e: KeyboardEvent) => this._handleKeydown(e)}
+ >
+ <header class="heading-3"><slot name="header"></slot></header>
+ <main>
+ <div class="overflow-container">
+ <slot name="main"></slot>
+ </div>
+ </main>
+ <footer>
+ <slot name="footer"></slot>
+ <gr-button
+ id="cancel"
+ class="${this.cancelLabel.length ? '' : 'hidden'}"
+ link
+ @click=${(e: Event) => this.handleCancelTap(e)}
+ >
+ ${this.cancelLabel}
+ </gr-button>
+ <gr-button
+ id="confirm"
+ link
+ primary
+ @click=${(e: Event) => this._handleConfirm(e)}
+ ?disabled=${this.disabled}
+ title=${this.confirmTooltip ?? ''}
+ >
+ ${this.confirmLabel}
+ </gr-button>
+ </footer>
+ </div>
+ `;
+ }
+
+ override updated(changedProperties: PropertyValues) {
+ if (changedProperties.has('confirmTooltip')) {
+ this.updateTooltip();
+ }
+ }
+
+ private updateTooltip() {
+ const confirmButton = this.confirmButton;
+ if (!confirmButton) return;
+ if (this.confirmTooltip) {
+ confirmButton.setAttribute('has-tooltip', 'true');
} else {
- this.$.confirm.removeAttribute('has-tooltip');
+ confirmButton.removeAttribute('has-tooltip');
}
}
@@ -98,7 +185,7 @@
);
}
- _handleCancelTap(e: Event) {
+ private handleCancelTap(e: Event) {
e.preventDefault();
e.stopPropagation();
this.dispatchEvent(
@@ -109,17 +196,13 @@
);
}
- _handleKeydown(e: KeydownEvent) {
+ _handleKeydown(e: KeyboardEvent) {
if (this.confirmOnEnter && e.keyCode === 13) {
this._handleConfirm(e);
}
}
resetFocus() {
- this.$.confirm.focus();
- }
-
- _computeCancelClass(cancelLabel: string) {
- return cancelLabel.length ? '' : 'hidden';
+ this.confirmButton!.focus();
}
}
diff --git a/polygerrit-ui/app/elements/shared/gr-dialog/gr-dialog_html.ts b/polygerrit-ui/app/elements/shared/gr-dialog/gr-dialog_html.ts
deleted file mode 100644
index a5cf8f1..0000000
--- a/polygerrit-ui/app/elements/shared/gr-dialog/gr-dialog_html.ts
+++ /dev/null
@@ -1,94 +0,0 @@
-/**
- * @license
- * Copyright (C) 2020 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 {html} from '@polymer/polymer/lib/utils/html-tag';
-
-export const htmlTemplate = html`
- <style include="gr-font-styles">
- /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
- </style>
- <style include="shared-styles">
- :host {
- color: var(--primary-text-color);
- display: block;
- max-height: 90vh;
- overflow: auto;
- }
- .container {
- display: flex;
- flex-direction: column;
- max-height: 90vh;
- padding: var(--spacing-xl);
- }
- header {
- flex-shrink: 0;
- padding-bottom: var(--spacing-xl);
- }
- main {
- display: flex;
- flex-shrink: 1;
- width: 100%;
- flex: 1;
- /* IMPORTANT: required for firefox */
- min-height: 0px;
- }
- main .overflow-container {
- flex: 1;
- overflow: auto;
- }
- footer {
- display: flex;
- flex-shrink: 0;
- justify-content: flex-end;
- padding-top: var(--spacing-xl);
- }
- gr-button {
- margin-left: var(--spacing-l);
- }
- .hidden {
- display: none;
- }
- </style>
- <div class="container" on-keydown="_handleKeydown">
- <header class="heading-3"><slot name="header"></slot></header>
- <main>
- <div class="overflow-container">
- <slot name="main"></slot>
- </div>
- </main>
- <footer>
- <slot name="footer"></slot>
- <gr-button
- id="cancel"
- class$="[[_computeCancelClass(cancelLabel)]]"
- link=""
- on-click="_handleCancelTap"
- >
- [[cancelLabel]]
- </gr-button>
- <gr-button
- id="confirm"
- link=""
- primary=""
- on-click="_handleConfirm"
- disabled="[[disabled]]"
- title$="[[confirmTooltip]]"
- >
- [[confirmLabel]]
- </gr-button>
- </footer>
- </div>
-`;
diff --git a/polygerrit-ui/app/elements/shared/gr-dialog/gr-dialog_test.ts b/polygerrit-ui/app/elements/shared/gr-dialog/gr-dialog_test.ts
index e7b7130..171fc6c 100644
--- a/polygerrit-ui/app/elements/shared/gr-dialog/gr-dialog_test.ts
+++ b/polygerrit-ui/app/elements/shared/gr-dialog/gr-dialog_test.ts
@@ -17,6 +17,7 @@
import * as MockInteractions from '@polymer/iron-test-helpers/mock-interactions';
import '../../../test/common-test-setup-karma';
+import './gr-dialog';
import {GrDialog} from './gr-dialog';
import {isHidden, queryAndAssert} from '../../../test/test-utils';
@@ -25,8 +26,9 @@
suite('gr-dialog tests', () => {
let element: GrDialog;
- setup(() => {
+ setup(async () => {
element = basicFixture.instantiate();
+ await element.updateComplete;
});
test('events', () => {
@@ -42,56 +44,59 @@
assert.equal(cancel.callCount, 1);
});
- test('confirmOnEnter', () => {
+ test('confirmOnEnter', async () => {
element.confirmOnEnter = false;
+ await element.updateComplete;
const handleConfirmStub = sinon.stub(element, '_handleConfirm');
const handleKeydownSpy = sinon.spy(element, '_handleKeydown');
- MockInteractions.pressAndReleaseKeyOn(
+ MockInteractions.keyDownOn(
queryAndAssert(element, 'main'),
13,
null,
'enter'
);
- flush();
+ await flush();
assert.isTrue(handleKeydownSpy.called);
assert.isFalse(handleConfirmStub.called);
element.confirmOnEnter = true;
- MockInteractions.pressAndReleaseKeyOn(
+ await element.updateComplete;
+
+ MockInteractions.keyDownOn(
queryAndAssert(element, 'main'),
13,
null,
'enter'
);
- flush();
+ await flush();
assert.isTrue(handleConfirmStub.called);
});
test('resetFocus', () => {
- const focusStub = sinon.stub(element.$.confirm, 'focus');
+ const focusStub = sinon.stub(element.confirmButton!, 'focus');
element.resetFocus();
assert.isTrue(focusStub.calledOnce);
});
suite('tooltip', () => {
test('tooltip not added by default', () => {
- assert.isNull(element.$.confirm.getAttribute('has-tooltip'));
+ assert.isNull(element.confirmButton!.getAttribute('has-tooltip'));
});
- test('tooltip added if confirm tooltip is passed', () => {
+ test('tooltip added if confirm tooltip is passed', async () => {
element.confirmTooltip = 'confirm tooltip';
- flush();
- assert(element.$.confirm.getAttribute('has-tooltip'));
+ await element.updateComplete;
+ assert(element.confirmButton!.getAttribute('has-tooltip'));
});
});
- test('empty cancel label hides cancel btn', () => {
+ test('empty cancel label hides cancel btn', async () => {
const cancelButton = queryAndAssert(element, '#cancel');
assert.isFalse(isHidden(cancelButton));
element.cancelLabel = '';
- flush();
+ await element.updateComplete;
assert.isTrue(isHidden(cancelButton));
});
diff --git a/polygerrit-ui/app/elements/shared/gr-dropdown-list/gr-dropdown-list.ts b/polygerrit-ui/app/elements/shared/gr-dropdown-list/gr-dropdown-list.ts
index ef46cec..6180f35 100644
--- a/polygerrit-ui/app/elements/shared/gr-dropdown-list/gr-dropdown-list.ts
+++ b/polygerrit-ui/app/elements/shared/gr-dropdown-list/gr-dropdown-list.ts
@@ -88,7 +88,7 @@
disabled = false;
@property({type: String, notify: true})
- value?: string;
+ value: string | number = '';
@property({type: Boolean})
showCopyForTriggerText = false;
@@ -122,6 +122,10 @@
return item.mobileText ? item.mobileText : item.text;
}
+ computeStringValue(val: string | number) {
+ return String(val);
+ }
+
@observe('value', 'items')
_handleValueChange(value?: string, items?: DropdownItem[]) {
if (!value || !items) {
diff --git a/polygerrit-ui/app/elements/shared/gr-dropdown-list/gr-dropdown-list_html.ts b/polygerrit-ui/app/elements/shared/gr-dropdown-list/gr-dropdown-list_html.ts
index 18a46a0..3875871 100644
--- a/polygerrit-ui/app/elements/shared/gr-dropdown-list/gr-dropdown-list_html.ts
+++ b/polygerrit-ui/app/elements/shared/gr-dropdown-list/gr-dropdown-list_html.ts
@@ -78,9 +78,8 @@
width: 100%;
}
gr-button {
- --gr-button: {
- @apply --trigger-style;
- }
+ font-family: var(--trigger-style-font-family);
+ --gr-button-text-color: var(--trigger-style-text-color);
}
gr-date-formatter {
color: var(--deemphasized-text-color);
@@ -123,6 +122,7 @@
class="dropdown-trigger"
on-click="_showDropdownTapHandler"
slot="dropdown-trigger"
+ no-uppercase
>
<span id="triggerText">[[text]]</span>
<gr-copy-clipboard
@@ -173,7 +173,10 @@
<gr-select bind-value="{{value}}">
<select>
<template is="dom-repeat" items="[[items]]">
- <option disabled$="[[item.disabled]]" value="[[item.value]]">
+ <option
+ disabled$="[[item.disabled]]"
+ value="[[computeStringValue(item.value)]]"
+ >
[[_computeMobileText(item)]]
</option>
</template>
diff --git a/polygerrit-ui/app/elements/shared/gr-dropdown/gr-dropdown_html.ts b/polygerrit-ui/app/elements/shared/gr-dropdown/gr-dropdown_html.ts
index 4bd98d6..3c07d94 100644
--- a/polygerrit-ui/app/elements/shared/gr-dropdown/gr-dropdown_html.ts
+++ b/polygerrit-ui/app/elements/shared/gr-dropdown/gr-dropdown_html.ts
@@ -33,7 +33,6 @@
}
gr-button {
vertical-align: top;
- @apply --gr-button;
}
gr-avatar {
height: 2em;
diff --git a/polygerrit-ui/app/elements/shared/gr-editable-label/gr-editable-label.ts b/polygerrit-ui/app/elements/shared/gr-editable-label/gr-editable-label.ts
index bd1046f..13b195e 100644
--- a/polygerrit-ui/app/elements/shared/gr-editable-label/gr-editable-label.ts
+++ b/polygerrit-ui/app/elements/shared/gr-editable-label/gr-editable-label.ts
@@ -26,12 +26,11 @@
import {IronDropdownElement} from '@polymer/iron-dropdown/iron-dropdown';
import {dom, EventApi} from '@polymer/polymer/lib/legacy/polymer.dom';
import {PaperInputElementExt} from '../../../types/types';
-import {CustomKeyboardEvent} from '../../../types/events';
+import {IronKeyboardEvent} from '../../../types/events';
import {
AutocompleteQuery,
GrAutocomplete,
} from '../gr-autocomplete/gr-autocomplete';
-import {getKeyboardEvent} from '../../../utils/dom-util';
const AWAIT_MAX_ITERS = 10;
const AWAIT_STEP = 5;
@@ -205,8 +204,8 @@
this.getGrAutocomplete()) as HTMLInputElement;
}
- _handleEnter(e: CustomKeyboardEvent) {
- e = getKeyboardEvent(e);
+ _handleEnter(event: IronKeyboardEvent) {
+ const e = event.detail.keyboardEvent;
const target = (dom(e) as EventApi).rootTarget;
if (target === this._nativeInput) {
e.preventDefault();
@@ -214,8 +213,8 @@
}
}
- _handleEsc(e: CustomKeyboardEvent) {
- e = getKeyboardEvent(e);
+ _handleEsc(event: IronKeyboardEvent) {
+ const e = event.detail.keyboardEvent;
const target = (dom(e) as EventApi).rootTarget;
if (target === this._nativeInput) {
e.preventDefault();
diff --git a/polygerrit-ui/app/elements/shared/gr-editable-label/gr-editable-label_html.ts b/polygerrit-ui/app/elements/shared/gr-editable-label/gr-editable-label_html.ts
index c303bed..e711e9d 100644
--- a/polygerrit-ui/app/elements/shared/gr-editable-label/gr-editable-label_html.ts
+++ b/polygerrit-ui/app/elements/shared/gr-editable-label/gr-editable-label_html.ts
@@ -72,7 +72,7 @@
--iron-icon-width: 18px;
}
gr-button.pencil {
- --padding: 0px 0px;
+ --gr-button-padding: 0px 0px;
}
</style>
<template is="dom-if" if="[[!showAsEditPencil]]">
diff --git a/polygerrit-ui/app/elements/shared/gr-file-status-chip/gr-file-status-chip.ts b/polygerrit-ui/app/elements/shared/gr-file-status-chip/gr-file-status-chip.ts
index 0f94b11..3f759b5 100644
--- a/polygerrit-ui/app/elements/shared/gr-file-status-chip/gr-file-status-chip.ts
+++ b/polygerrit-ui/app/elements/shared/gr-file-status-chip/gr-file-status-chip.ts
@@ -36,7 +36,7 @@
@property({type: Object})
file?: NormalizedFileInfo;
- static get styles() {
+ static override get styles() {
return [
sharedStyles,
css`
@@ -65,7 +65,7 @@
];
}
- render() {
+ override render() {
return html` <span
class="${this._computeStatusClass(this.file)}"
tabindex="0"
diff --git a/polygerrit-ui/app/elements/shared/gr-hovercard-account/gr-hovercard-account.ts b/polygerrit-ui/app/elements/shared/gr-hovercard-account/gr-hovercard-account.ts
index a329736..6a34fbb 100644
--- a/polygerrit-ui/app/elements/shared/gr-hovercard-account/gr-hovercard-account.ts
+++ b/polygerrit-ui/app/elements/shared/gr-hovercard-account/gr-hovercard-account.ts
@@ -16,16 +16,11 @@
*/
import '@polymer/iron-icon/iron-icon';
-import '../../../styles/gr-font-styles';
-import '../../../styles/shared-styles';
import '../gr-avatar/gr-avatar';
import '../gr-button/gr-button';
-import {HovercardBehaviorMixin} from '../gr-hovercard/gr-hovercard-behavior';
-import {PolymerElement} from '@polymer/polymer/polymer-element';
-import {htmlTemplate} from './gr-hovercard-account_html';
import {appContext} from '../../../services/app-context';
import {accountKey, isSelf} from '../../../utils/account-util';
-import {customElement, property} from '@polymer/decorators';
+import {customElement, property} from 'lit/decorators';
import {
AccountInfo,
ChangeInfo,
@@ -45,16 +40,15 @@
import {CURRENT} from '../../../utils/patch-set-util';
import {isInvolved, isRemovableReviewer} from '../../../utils/change-util';
import {assertIsDefined} from '../../../utils/common-util';
+import {fontStyles} from '../../../styles/gr-font-styles';
+import {css, html, LitElement} from 'lit';
+import {HovercardMixin} from '../../../mixins/hovercard-mixin/hovercard-mixin';
// This avoids JSC_DYNAMIC_EXTENDS_WITHOUT_JSDOC closure compiler error.
-const base = HovercardBehaviorMixin(PolymerElement);
+const base = HovercardMixin(LitElement);
@customElement('gr-hovercard-account')
export class GrHovercardAccount extends base {
- static get template() {
- return htmlTemplate;
- }
-
@property({type: Object})
account!: AccountInfo;
@@ -96,7 +90,7 @@
this.reporting = appContext.reportingService;
}
- connectedCallback() {
+ override connectedCallback() {
super.connectedCallback();
this.restApiService.getConfig().then(config => {
this._config = config;
@@ -106,9 +100,214 @@
});
}
- _computeText(account?: AccountInfo, selfAccount?: AccountInfo) {
- if (!account || !selfAccount) return '';
- return isSelf(account, selfAccount) ? 'Your' : 'Their';
+ static override get styles() {
+ return [
+ fontStyles,
+ base.styles || [],
+ css`
+ .top,
+ .attention,
+ .status,
+ .voteable {
+ padding: var(--spacing-s) var(--spacing-l);
+ }
+ .top {
+ display: flex;
+ padding-top: var(--spacing-xl);
+ min-width: 300px;
+ }
+ gr-avatar {
+ height: 48px;
+ width: 48px;
+ margin-right: var(--spacing-l);
+ }
+ .title,
+ .email {
+ color: var(--deemphasized-text-color);
+ }
+ .action {
+ border-top: 1px solid var(--border-color);
+ padding: var(--spacing-s) var(--spacing-l);
+ --gr-button-padding: var(--spacing-s) var(--spacing-m);
+ }
+ .attention {
+ background-color: var(--emphasis-color);
+ }
+ .attention a {
+ text-decoration: none;
+ }
+ iron-icon {
+ vertical-align: top;
+ }
+ .status iron-icon {
+ width: 14px;
+ height: 14px;
+ position: relative;
+ top: 2px;
+ }
+ iron-icon.attentionIcon {
+ width: 14px;
+ height: 14px;
+ position: relative;
+ top: 3px;
+ }
+ .reason {
+ padding-top: var(--spacing-s);
+ }
+ `,
+ ];
+ }
+
+ override render() {
+ return html`
+ <div id="container" role="tooltip" tabindex="-1">
+ ${this.renderContent()}
+ </div>
+ `;
+ }
+
+ private renderContent() {
+ if (!this._isShowing) return;
+ return html`
+ <div class="top">
+ <div class="avatar">
+ <gr-avautar .account=${this.account} imageSize="56"></gr-avatar>
+ </div>
+ <div class="account">
+ <h3 class="name heading-3">${this.account.name}</h3>
+ <div class="email">${this.account.email}</div>
+ </div>
+ </div>
+ ${this.renderAccountStatus()}
+ ${
+ this.voteableText
+ ? html`
+ <div class="voteable">
+ <span class="title">Voteable:</span>
+ <span class="value">${this.voteableText}</span>
+ </div>
+ `
+ : ''
+ }
+ ${this.renderNeedsAttention()} ${this.renderAddToAttention()}
+ ${this.renderRemoveFromAttention()} ${this.renderReviewerOrCcActions()}
+ `;
+ }
+
+ private renderReviewerOrCcActions() {
+ if (!this._selfAccount || !isRemovableReviewer(this.change, this.account))
+ return;
+ return html`
+ <div class="action">
+ <gr-button
+ class="removeReviewerOrCC"
+ link=""
+ no-uppercase
+ @click="${this.handleRemoveReviewerOrCC}"
+ >
+ Remove ${this.computeReviewerOrCCText()}
+ </gr-button>
+ </div>
+ <div class="action">
+ <gr-button
+ class="changeReviewerOrCC"
+ link=""
+ no-uppercase
+ @click="${this.handleChangeReviewerOrCCStatus}"
+ >
+ ${this.computeChangeReviewerOrCCText()}
+ </gr-button>
+ </div>
+ `;
+ }
+
+ private renderAccountStatus() {
+ if (!this.account.status) return;
+ return html`
+ <div class="status">
+ <span class="title">
+ <iron-icon icon="gr-icons:calendar"></iron-icon>
+ Status:
+ </span>
+ <span class="value">${this.account.status}</span>
+ </div>
+ `;
+ }
+
+ private renderNeedsAttention() {
+ if (!(this.isAttentionEnabled && this.hasUserAttention)) return;
+ const lastUpdate = getLastUpdate(this.account, this.change);
+ return html`
+ <div class="attention">
+ <div>
+ <iron-icon
+ class="attentionIcon"
+ icon="gr-icons:attention"
+ ></iron-icon>
+ <span> ${this.computePronoun()} turn to take this action. </span>
+ <a
+ href="https://gerrit-review.googlesource.com/Documentation/user-attention-set.html"
+ target="_blank"
+ >
+ <iron-icon
+ icon="gr-icons:help-outline"
+ title="read documentation"
+ ></iron-icon>
+ </a>
+ </div>
+ <div class="reason">
+ <span class="title">Reason:</span>
+ <span class="value">
+ ${getReason(this._config, this.account, this.change)}
+ </span>
+ ${lastUpdate
+ ? html` (<gr-date-formatter
+ withTooltip
+ .dateStr="${lastUpdate}"
+ ></gr-date-formatter
+ >)`
+ : ''}
+ </div>
+ </div>
+ `;
+ }
+
+ private renderAddToAttention() {
+ if (!this.computeShowActionAddToAttentionSet()) return;
+ return html`
+ <div class="action">
+ <gr-button
+ class="addToAttentionSet"
+ link=""
+ no-uppercase
+ @click="${this.handleClickAddToAttentionSet}"
+ >
+ Add to attention set
+ </gr-button>
+ </div>
+ `;
+ }
+
+ private renderRemoveFromAttention() {
+ if (!this.computeShowActionRemoveFromAttentionSet()) return;
+ return html`
+ <div class="action">
+ <gr-button
+ class="removeFromAttentionSet"
+ link=""
+ no-uppercase
+ @click="${this.handleClickRemoveFromAttentionSet}"
+ >
+ Remove from attention set
+ </gr-button>
+ </div>
+ `;
+ }
+
+ // private but used by tests
+ computePronoun() {
+ if (!this.account || !this._selfAccount) return '';
+ return isSelf(this.account, this._selfAccount) ? 'Your' : 'Their';
}
get isAttentionEnabled() {
@@ -123,22 +322,11 @@
return hasAttention(this.account, this.change);
}
- _computeReason(change?: ChangeInfo) {
- return getReason(this._config, this.account, change);
- }
-
- _computeLastUpdate(change?: ChangeInfo) {
- return getLastUpdate(this.account, change);
- }
-
- _showReviewerOrCCActions(account?: AccountInfo, change?: ChangeInfo) {
- return !!this._selfAccount && isRemovableReviewer(change, account);
- }
-
- _getReviewerState(account: AccountInfo, change: ChangeInfo) {
+ private getReviewerState() {
if (
- change.reviewers[ReviewerState.REVIEWER]?.some(
- (reviewer: AccountInfo) => reviewer._account_id === account._account_id
+ this.change!.reviewers[ReviewerState.REVIEWER]?.some(
+ (reviewer: AccountInfo) =>
+ reviewer._account_id === this.account._account_id
)
) {
return ReviewerState.REVIEWER;
@@ -146,21 +334,21 @@
return ReviewerState.CC;
}
- _computeReviewerOrCCText(account?: AccountInfo, change?: ChangeInfo) {
- if (!change || !account) return '';
- return this._getReviewerState(account, change) === ReviewerState.REVIEWER
+ private computeReviewerOrCCText() {
+ if (!this.change || !this.account) return '';
+ return this.getReviewerState() === ReviewerState.REVIEWER
? 'Reviewer'
: 'CC';
}
- _computeChangeReviewerOrCCText(account?: AccountInfo, change?: ChangeInfo) {
- if (!change || !account) return '';
- return this._getReviewerState(account, change) === ReviewerState.REVIEWER
+ private computeChangeReviewerOrCCText() {
+ if (!this.change || !this.account) return '';
+ return this.getReviewerState() === ReviewerState.REVIEWER
? 'Move Reviewer to CC'
: 'Move CC to Reviewer';
}
- _handleChangeReviewerOrCCStatus() {
+ private handleChangeReviewerOrCCStatus() {
assertIsDefined(this.change, 'change');
// accountKey() throws an error if _account_id & email is not found, which
// we want to check before showing reloading toast
@@ -173,7 +361,7 @@
{
reviewer: _accountKey,
state:
- this._getReviewerState(this.account, this.change) === ReviewerState.CC
+ this.getReviewerState() === ReviewerState.CC
? ReviewerState.REVIEWER
: ReviewerState.CC,
},
@@ -184,15 +372,14 @@
.then(response => {
if (!response || !response.ok) {
throw new Error(
- 'something went wrong when toggling' +
- this._getReviewerState(this.account, this.change!)
+ 'something went wrong when toggling' + this.getReviewerState()
);
}
this.dispatchEventThroughTarget('reload', {clearPatchset: true});
});
}
- _handleRemoveReviewerOrCC() {
+ private handleRemoveReviewerOrCC() {
if (!this.change || !(this.account?._account_id || this.account?.email))
throw new Error('Missing change or account.');
this.dispatchEventThroughTarget('show-alert', {
@@ -212,25 +399,21 @@
});
}
- _computeShowLabelNeedsAttention() {
- return this.isAttentionEnabled && this.hasUserAttention;
- }
-
- _computeShowActionAddToAttentionSet() {
+ private computeShowActionAddToAttentionSet() {
const involvedOrSelf =
isInvolved(this.change, this._selfAccount) ||
isSelf(this.account, this._selfAccount);
return involvedOrSelf && this.isAttentionEnabled && !this.hasUserAttention;
}
- _computeShowActionRemoveFromAttentionSet() {
+ private computeShowActionRemoveFromAttentionSet() {
const involvedOrSelf =
isInvolved(this.change, this._selfAccount) ||
isSelf(this.account, this._selfAccount);
return involvedOrSelf && this.isAttentionEnabled && this.hasUserAttention;
}
- _handleClickAddToAttentionSet() {
+ private handleClickAddToAttentionSet(e: MouseEvent) {
if (!this.change || !this.account._account_id) return;
this.dispatchEventThroughTarget('show-alert', {
message: 'Saving attention set update ...',
@@ -251,17 +434,17 @@
this.reporting.reportInteraction(
'attention-hovercard-add',
- this._reportingDetails()
+ this.reportingDetails()
);
this.restApiService
.addToAttentionSet(this.change._number, this.account._account_id, reason)
.then(() => {
this.dispatchEventThroughTarget('hide-alert');
});
- this.hide();
+ this.hide(e);
}
- _handleClickRemoveFromAttentionSet() {
+ private handleClickRemoveFromAttentionSet(e: MouseEvent) {
if (!this.change || !this.account._account_id) return;
this.dispatchEventThroughTarget('show-alert', {
message: 'Saving attention set update ...',
@@ -278,7 +461,7 @@
this.reporting.reportInteraction(
'attention-hovercard-remove',
- this._reportingDetails()
+ this.reportingDetails()
);
this.restApiService
.removeFromAttentionSet(
@@ -289,10 +472,10 @@
.then(() => {
this.dispatchEventThroughTarget('hide-alert');
});
- this.hide();
+ this.hide(e);
}
- _reportingDetails() {
+ private reportingDetails() {
const targetId = this.account._account_id;
const ownerId =
(this.change && this.change.owner && this.change.owner._account_id) || -1;
diff --git a/polygerrit-ui/app/elements/shared/gr-hovercard-account/gr-hovercard-account_html.ts b/polygerrit-ui/app/elements/shared/gr-hovercard-account/gr-hovercard-account_html.ts
deleted file mode 100644
index f1f6bf8..0000000
--- a/polygerrit-ui/app/elements/shared/gr-hovercard-account/gr-hovercard-account_html.ts
+++ /dev/null
@@ -1,196 +0,0 @@
-/**
- * @license
- * Copyright (C) 2020 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 '../gr-hovercard/gr-hovercard-shared-style';
-import {html} from '@polymer/polymer/lib/utils/html-tag';
-
-export const htmlTemplate = html`
- <style include="gr-font-styles">
- /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
- </style>
- <style include="gr-hovercard-shared-style">
- .top,
- .attention,
- .status,
- .voteable {
- padding: var(--spacing-s) var(--spacing-l);
- }
- .top {
- display: flex;
- padding-top: var(--spacing-xl);
- min-width: 300px;
- }
- gr-avatar {
- height: 48px;
- width: 48px;
- margin-right: var(--spacing-l);
- }
- .title,
- .email {
- color: var(--deemphasized-text-color);
- }
- .action {
- border-top: 1px solid var(--border-color);
- padding: var(--spacing-s) var(--spacing-l);
- --gr-button: {
- padding: var(--spacing-s) var(--spacing-m);
- }
- }
- .attention {
- background-color: var(--emphasis-color);
- }
- .attention a {
- text-decoration: none;
- }
- iron-icon {
- vertical-align: top;
- }
- .status iron-icon {
- width: 14px;
- height: 14px;
- position: relative;
- top: 2px;
- }
- iron-icon.attentionIcon {
- width: 14px;
- height: 14px;
- position: relative;
- top: 3px;
- }
- .reason {
- padding-top: var(--spacing-s);
- }
- </style>
- <div id="container" role="tooltip" tabindex="-1">
- <template is="dom-if" if="[[_isShowing]]">
- <div class="top">
- <div class="avatar">
- <gr-avatar account="[[account]]" imageSize="56"></gr-avatar>
- </div>
- <div class="account">
- <h3 class="name heading-3">[[account.name]]</h3>
- <div class="email">[[account.email]]</div>
- </div>
- </div>
- <template is="dom-if" if="[[account.status]]">
- <div class="status">
- <span class="title">
- <iron-icon icon="gr-icons:calendar"></iron-icon>
- Status:
- </span>
- <span class="value">[[account.status]]</span>
- </div>
- </template>
- <template is="dom-if" if="[[voteableText]]">
- <div class="voteable">
- <span class="title">Voteable:</span>
- <span class="value">[[voteableText]]</span>
- </div>
- </template>
- <template
- is="dom-if"
- if="[[_computeShowLabelNeedsAttention(_config, highlightAttention, account, change)]]"
- >
- <div class="attention">
- <div>
- <iron-icon
- class="attentionIcon"
- icon="gr-icons:attention"
- ></iron-icon>
- <span>
- [[_computeText(account, _selfAccount)]] turn to take action.
- </span>
- <a
- href="https://gerrit-review.googlesource.com/Documentation/user-attention-set.html"
- target="_blank"
- >
- <iron-icon
- icon="gr-icons:help-outline"
- title="read documentation"
- ></iron-icon>
- </a>
- </div>
- <div class="reason">
- <span class="title">Reason:</span>
- <span class="value">[[_computeReason(change)]]</span>
- <template is="dom-if" if="[[_computeLastUpdate(change)]]">
- (<gr-date-formatter
- has-tooltip
- date-str="[[_computeLastUpdate(change)]]"
- ></gr-date-formatter
- >)
- </template>
- </div>
- </div>
- </template>
- <template
- is="dom-if"
- if="[[_computeShowActionAddToAttentionSet(_config, highlightAttention, account, change, _selfAccount)]]"
- >
- <div class="action">
- <gr-button
- class="addToAttentionSet"
- link=""
- no-uppercase=""
- on-click="_handleClickAddToAttentionSet"
- >
- Add to attention set
- </gr-button>
- </div>
- </template>
- <template
- is="dom-if"
- if="[[_computeShowActionRemoveFromAttentionSet(_config, highlightAttention, account, change, _selfAccount)]]"
- >
- <div class="action">
- <gr-button
- class="removeFromAttentionSet"
- link=""
- no-uppercase=""
- on-click="_handleClickRemoveFromAttentionSet"
- >
- Remove from attention set
- </gr-button>
- </div>
- </template>
- <template
- is="dom-if"
- if="[[_showReviewerOrCCActions(account, change, _selfAccount)]]"
- >
- <div class="action">
- <gr-button
- class="removeReviewerOrCC"
- link=""
- no-uppercase=""
- on-click="_handleRemoveReviewerOrCC"
- >
- Remove [[_computeReviewerOrCCText(account, change)]]
- </gr-button>
- </div>
- <div class="action">
- <gr-button
- class="changeReviewerOrCC"
- link=""
- no-uppercase=""
- on-click="_handleChangeReviewerOrCCStatus"
- >
- [[_computeChangeReviewerOrCCText(account, change)]]
- </gr-button>
- </div>
- </template>
- </template>
- </div>
-`;
diff --git a/polygerrit-ui/app/elements/shared/gr-hovercard-account/gr-hovercard-account_test.js b/polygerrit-ui/app/elements/shared/gr-hovercard-account/gr-hovercard-account_test.js
index e9a224c..5530d7c 100644
--- a/polygerrit-ui/app/elements/shared/gr-hovercard-account/gr-hovercard-account_test.js
+++ b/polygerrit-ui/app/elements/shared/gr-hovercard-account/gr-hovercard-account_test.js
@@ -19,7 +19,7 @@
import './gr-hovercard-account.js';
import {html} from '@polymer/polymer/lib/utils/html-tag.js';
import {ReviewerState} from '../../../constants/constants.js';
-import {stubRestApi} from '../../../test/test-utils.js';
+import {mockPromise, stubRestApi} from '../../../test/test-utils.js';
const basicFixture = fixtureFromTemplate(html`
<gr-hovercard-account class="hovered"></gr-hovercard-account>
@@ -57,33 +57,21 @@
'Kermit The Frog');
});
- test('_computeLastUpdate', () => {
- const last_update = '2019-07-17 19:39:02.000000000';
- const change = {
- attention_set: {
- 31415926535: {
- last_update,
- },
- },
- };
- assert.equal(element._computeLastUpdate(change), last_update);
- });
-
- test('_computeText', () => {
- let account = {_account_id: '1'};
- const selfAccount = {_account_id: '1'};
- assert.equal(element._computeText(account, selfAccount), 'Your');
- account = {_account_id: '2'};
- assert.equal(element._computeText(account, selfAccount), 'Their');
+ test('computePronoun', () => {
+ element.account = {_account_id: '1'};
+ element._selfAccount = {_account_id: '1'};
+ assert.equal(element.computePronoun(), 'Your');
+ element.account = {_account_id: '2'};
+ assert.equal(element.computePronoun(), 'Their');
});
test('account status is not shown if the property is not set', () => {
assert.isNull(element.shadowRoot.querySelector('.status'));
});
- test('account status is displayed', () => {
+ test('account status is displayed', async () => {
element.account = {status: 'OOO', ...ACCOUNT};
- flush();
+ await element.updateComplete;
assert.equal(element.shadowRoot.querySelector('.status .value').innerText,
'OOO');
});
@@ -92,9 +80,9 @@
assert.isNull(element.shadowRoot.querySelector('.voteable'));
});
- test('voteable div is displayed', () => {
+ test('voteable div is displayed', async () => {
element.voteableText = 'CodeReview: +2';
- flush();
+ await element.updateComplete;
assert.equal(element.shadowRoot.querySelector('.voteable .value').innerText,
element.voteableText);
});
@@ -106,15 +94,15 @@
[ReviewerState.REVIEWER]: [ACCOUNT],
},
};
+ await element.updateComplete;
stubRestApi('removeChangeReviewer').returns(Promise.resolve({ok: true}));
const reloadListener = sinon.spy();
element._target.addEventListener('reload', reloadListener);
- flush();
const button = element.shadowRoot.querySelector('.removeReviewerOrCC');
assert.isOk(button);
assert.equal(button.innerText, 'Remove Reviewer');
MockInteractions.tap(button);
- await flush();
+ await element.updateComplete;
assert.isTrue(reloadListener.called);
});
@@ -125,6 +113,7 @@
[ReviewerState.REVIEWER]: [ACCOUNT],
},
};
+ await element.updateComplete;
const saveReviewStub = stubRestApi(
'saveChangeReview').returns(
Promise.resolve({ok: true}));
@@ -132,14 +121,12 @@
const reloadListener = sinon.spy();
element._target.addEventListener('reload', reloadListener);
- flush();
const button = element.shadowRoot.querySelector('.changeReviewerOrCC');
assert.isOk(button);
assert.equal(button.innerText, 'Move Reviewer to CC');
MockInteractions.tap(button);
- await flush();
-
+ await element.updateComplete;
assert.isTrue(saveReviewStub.called);
assert.isTrue(reloadListener.called);
});
@@ -151,20 +138,19 @@
[ReviewerState.REVIEWER]: [],
},
};
+ await element.updateComplete;
const saveReviewStub = stubRestApi(
'saveChangeReview').returns(Promise.resolve({ok: true}));
stubRestApi('removeChangeReviewer').returns(Promise.resolve({ok: true}));
const reloadListener = sinon.spy();
element._target.addEventListener('reload', reloadListener);
- flush();
const button = element.shadowRoot.querySelector('.changeReviewerOrCC');
assert.isOk(button);
assert.equal(button.innerText, 'Move CC to Reviewer');
MockInteractions.tap(button);
- await flush();
-
+ await element.updateComplete;
assert.isTrue(saveReviewStub.called);
assert.isTrue(reloadListener.called);
});
@@ -176,31 +162,26 @@
[ReviewerState.REVIEWER]: [],
},
};
+ await element.updateComplete;
stubRestApi('removeChangeReviewer').returns(Promise.resolve({ok: true}));
const reloadListener = sinon.spy();
element._target.addEventListener('reload', reloadListener);
- flush();
const button = element.shadowRoot.querySelector('.removeReviewerOrCC');
assert.equal(button.innerText, 'Remove CC');
assert.isOk(button);
MockInteractions.tap(button);
-
- await flush();
-
+ await element.updateComplete;
assert.isTrue(reloadListener.called);
});
test('add to attention set', async () => {
- let apiResolve;
- const apiPromise = new Promise(r => {
- apiResolve = r;
- });
+ const apiPromise = mockPromise();
const apiSpy = stubRestApi('addToAttentionSet').returns(apiPromise);
element.highlightAttention = true;
element._target = document.createElement('div');
- flush();
+ await element.updateComplete;
const showAlertListener = sinon.spy();
const hideAlertListener = sinon.spy();
const updatedListener = sinon.spy();
@@ -224,8 +205,8 @@
assert.isTrue(updatedListener.called, 'updatedListener was called');
assert.isFalse(element._isShowing, 'hovercard is hidden');
- apiResolve({});
- await flush();
+ apiPromise.resolve({});
+ await element.updateComplete;
assert.isTrue(apiSpy.calledOnce);
assert.equal(apiSpy.lastCall.args[2],
`Added by <GERRIT_ACCOUNT_${ACCOUNT._account_id}>`
@@ -234,10 +215,7 @@
});
test('remove from attention set', async () => {
- let apiResolve;
- const apiPromise = new Promise(r => {
- apiResolve = r;
- });
+ const apiPromise = mockPromise();
const apiSpy = stubRestApi('removeFromAttentionSet').returns(apiPromise);
element.highlightAttention = true;
element.change = {
@@ -246,7 +224,7 @@
owner: {...ACCOUNT},
};
element._target = document.createElement('div');
- flush();
+ await element.updateComplete;
const showAlertListener = sinon.spy();
const hideAlertListener = sinon.spy();
const updatedListener = sinon.spy();
@@ -264,8 +242,8 @@
assert.isTrue(updatedListener.called, 'updatedListener was called');
assert.isFalse(element._isShowing, 'hovercard is hidden');
- apiResolve({});
- await flush();
+ apiPromise.resolve({});
+ await element.updateComplete;
assert.isTrue(apiSpy.calledOnce);
assert.equal(apiSpy.lastCall.args[2],
diff --git a/polygerrit-ui/app/elements/shared/gr-hovercard/gr-hovercard-shared-style.ts b/polygerrit-ui/app/elements/shared/gr-hovercard/gr-hovercard-shared-style.ts
deleted file mode 100644
index aa92654..0000000
--- a/polygerrit-ui/app/elements/shared/gr-hovercard/gr-hovercard-shared-style.ts
+++ /dev/null
@@ -1,51 +0,0 @@
-/**
- * @license
- * Copyright (C) 2020 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.
- */
-
-// Mark the file as a module. Otherwise typescript assumes this is a script
-// and $_documentContainer is a global variable.
-// See: https://www.typescriptlang.org/docs/handbook/modules.html
-export {};
-
-/** The shared styles for all hover cards. */
-const GrHoverCardSharedStyle = document.createElement('dom-module');
-GrHoverCardSharedStyle.innerHTML = `<template>
- <style include="shared-styles">
- :host {
- position: absolute;
- display: none;
- z-index: 200;
- max-width: 600px;
- outline: none;
- }
- :host(.hovered) {
- display: block;
- }
- :host(.hide) {
- visibility: hidden;
- }
- /* You have to use a <div class="container"> in your hovercard in order
- to pick up this consistent styling. */
- #container {
- background: var(--dialog-background-color);
- border: 1px solid var(--border-color);
- border-radius: var(--border-radius);
- box-shadow: var(--elevation-level-5);
- }
- </style>
- </template>`;
-
-GrHoverCardSharedStyle.register('gr-hovercard-shared-style');
diff --git a/polygerrit-ui/app/elements/shared/gr-hovercard/gr-hovercard.ts b/polygerrit-ui/app/elements/shared/gr-hovercard/gr-hovercard.ts
index acc5e15..bf35c06 100644
--- a/polygerrit-ui/app/elements/shared/gr-hovercard/gr-hovercard.ts
+++ b/polygerrit-ui/app/elements/shared/gr-hovercard/gr-hovercard.ts
@@ -15,20 +15,32 @@
* limitations under the License.
*/
-import '../../../styles/shared-styles';
-import {PolymerElement} from '@polymer/polymer/polymer-element';
-import {htmlTemplate} from './gr-hovercard_html';
-import {HovercardBehaviorMixin} from './gr-hovercard-behavior';
-import './gr-hovercard-shared-style';
-import {customElement} from '@polymer/decorators';
+import {customElement} from 'lit/decorators';
+import {HovercardMixin} from '../../../mixins/hovercard-mixin/hovercard-mixin';
+import {css, html, LitElement} from 'lit';
// This avoids JSC_DYNAMIC_EXTENDS_WITHOUT_JSDOC closure compiler error.
-const base = HovercardBehaviorMixin(PolymerElement);
+const base = HovercardMixin(LitElement);
@customElement('gr-hovercard')
export class GrHovercard extends base {
- static get template() {
- return htmlTemplate;
+ static override get styles() {
+ return [
+ base.styles ?? [],
+ css`
+ #container {
+ padding: var(--spacing-l);
+ }
+ `,
+ ];
+ }
+
+ override render() {
+ return html`
+ <div id="container" role="tooltip" tabindex="-1">
+ <slot></slot>
+ </div>
+ `;
}
}
diff --git a/polygerrit-ui/app/elements/shared/gr-hovercard/gr-hovercard_html.ts b/polygerrit-ui/app/elements/shared/gr-hovercard/gr-hovercard_html.ts
deleted file mode 100644
index 830cbd878..0000000
--- a/polygerrit-ui/app/elements/shared/gr-hovercard/gr-hovercard_html.ts
+++ /dev/null
@@ -1,28 +0,0 @@
-/**
- * @license
- * Copyright (C) 2020 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 {html} from '@polymer/polymer/lib/utils/html-tag';
-
-export const htmlTemplate = html`
- <style include="gr-hovercard-shared-style">
- #container {
- padding: var(--spacing-l);
- }
- </style>
- <div id="container" role="tooltip" tabindex="-1">
- <slot></slot>
- </div>
-`;
diff --git a/polygerrit-ui/app/elements/shared/gr-hovercard/gr-hovercard_test.js b/polygerrit-ui/app/elements/shared/gr-hovercard/gr-hovercard_test.js
deleted file mode 100644
index 27ef23f..0000000
--- a/polygerrit-ui/app/elements/shared/gr-hovercard/gr-hovercard_test.js
+++ /dev/null
@@ -1,166 +0,0 @@
-/**
- * @license
- * Copyright (C) 2018 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 './gr-hovercard.js';
-import {html} from '@polymer/polymer/lib/utils/html-tag.js';
-
-const basicFixture = fixtureFromTemplate(html`
-<gr-hovercard for="foo" id="bar"></gr-hovercard>
-`);
-
-suite('gr-hovercard tests', () => {
- let element;
-
- let button;
- let testResolve;
- let testPromise;
-
- setup(() => {
- testResolve = undefined;
- testPromise = new Promise(r => testResolve = r);
- button = document.createElement('button');
- button.innerHTML = 'Hello';
- button.setAttribute('id', 'foo');
- document.body.appendChild(button);
-
- element = basicFixture.instantiate();
- });
-
- teardown(() => {
- element.hide({});
- button.remove();
- });
-
- test('updatePosition', () => {
- // Test that the correct style properties have at least been set.
- element.position = 'bottom';
- element.updatePosition();
- assert.typeOf(element.style.getPropertyValue('left'), 'string');
- assert.typeOf(element.style.getPropertyValue('top'), 'string');
- assert.typeOf(element.style.getPropertyValue('paddingTop'), 'string');
- assert.typeOf(element.style.getPropertyValue('marginTop'), 'string');
-
- const parentRect = document.documentElement.getBoundingClientRect();
- const targetRect = element._target.getBoundingClientRect();
- const thisRect = element.getBoundingClientRect();
-
- const targetLeft = targetRect.left - parentRect.left;
- const targetTop = targetRect.top - parentRect.top;
-
- const pixelCompare = pixel =>
- Math.round(parseInt(pixel.substring(0, pixel.length - 1)), 10);
-
- assert.equal(
- pixelCompare(element.style.left),
- pixelCompare(
- (targetLeft + (targetRect.width - thisRect.width) / 2) + 'px'));
- assert.equal(
- pixelCompare(element.style.top),
- pixelCompare(
- (targetTop + targetRect.height + element.offset) + 'px'));
- });
-
- test('hide', () => {
- element.hide({});
- const style = getComputedStyle(element);
- assert.isFalse(element._isShowing);
- assert.isFalse(element.classList.contains('hovered'));
- assert.equal(style.display, 'none');
- assert.notEqual(element.container, element.parentNode);
- });
-
- test('show', () => {
- element.show({});
- const style = getComputedStyle(element);
- assert.isTrue(element._isShowing);
- assert.isTrue(element.classList.contains('hovered'));
- assert.equal(style.opacity, '1');
- assert.equal(style.visibility, 'visible');
- });
-
- test('debounceShow does not show immediately', async () => {
- element.debounceShowBy(100);
- setTimeout(testResolve, 0);
- await testPromise;
- assert.isFalse(element._isShowing);
- });
-
- test('debounceShow shows after delay', async () => {
- element.debounceShowBy(1);
- setTimeout(testResolve, 10);
- await testPromise;
- assert.isTrue(element._isShowing);
- });
-
- test('card is scheduled to show on enter and hides on leave', async () => {
- const button = document.querySelector('button');
- let enterResolve = undefined;
- const enterPromise = new Promise(r => enterResolve = r);
- button.addEventListener('mouseenter', enterResolve);
- let leaveResolve = undefined;
- const leavePromise = new Promise(r => leaveResolve = r);
- button.addEventListener('mouseleave', leaveResolve);
-
- assert.isFalse(element._isShowing);
- button.dispatchEvent(new CustomEvent('mouseenter'));
-
- await enterPromise;
- assert.isTrue(element.isScheduledToShow);
- element.showTask.flush();
- assert.isTrue(element._isShowing);
- assert.isFalse(element.isScheduledToShow);
-
- button.dispatchEvent(new CustomEvent('mouseleave'));
-
- await leavePromise;
- assert.isTrue(element.isScheduledToHide);
- assert.isTrue(element._isShowing);
- element.hideTask.flush();
- assert.isFalse(element.isScheduledToShow);
- assert.isFalse(element._isShowing);
-
- button.removeEventListener('mouseenter', enterResolve);
- button.removeEventListener('mouseleave', leaveResolve);
- });
-
- test('card should disappear on click', async () => {
- const button = document.querySelector('button');
- let enterResolve = undefined;
- const enterPromise = new Promise(r => enterResolve = r);
- button.addEventListener('mouseenter', enterResolve);
- let clickResolve = undefined;
- const clickPromise = new Promise(r => clickResolve = r);
- button.addEventListener('click', clickResolve);
-
- assert.isFalse(element._isShowing);
-
- button.dispatchEvent(new CustomEvent('mouseenter'));
-
- await enterPromise;
- assert.isTrue(element.isScheduledToShow);
- MockInteractions.tap(button);
-
- await clickPromise;
- assert.isFalse(element.isScheduledToShow);
- assert.isFalse(element._isShowing);
-
- button.removeEventListener('mouseenter', enterResolve);
- button.removeEventListener('click', clickResolve);
- });
-});
-
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-change-actions-js-api_test.js b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-change-actions-js-api_test.js
index 203784d..87f6052 100644
--- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-change-actions-js-api_test.js
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-change-actions-js-api_test.js
@@ -125,7 +125,7 @@
.querySelector('[data-action-key="' + key + '"]'));
});
- test('action button properties', () => {
+ test('action button properties', async () => {
const key = changeActions.add(changeActions.ActionType.REVISION, 'Bork!');
flush();
const button = element.shadowRoot
@@ -137,17 +137,17 @@
changeActions.setTitle(key, 'Yo hint');
changeActions.setEnabled(key, false);
changeActions.setIcon(key, 'pupper');
- flush();
+ await flush();
assert.equal(button.getAttribute('data-label'), 'Yo');
- assert.equal(button.getAttribute('title'), 'Yo hint');
+ assert.equal(button.parentElement.getAttribute('title'), 'Yo hint');
assert.isTrue(button.disabled);
assert.equal(button.querySelector('iron-icon').icon,
'gr-icons:pupper');
});
- test('hide action buttons', () => {
+ test('hide action buttons', async () => {
const key = changeActions.add(changeActions.ActionType.REVISION, 'Bork!');
- flush();
+ await flush();
let button = element.shadowRoot
.querySelector('[data-action-key="' + key + '"]');
assert.isOk(button);
@@ -168,7 +168,7 @@
.querySelector('[data-action-key="' + key + '"]'));
changeActions.setActionOverflow(
changeActions.ActionType.REVISION, key, true);
- flush();
+ await flush();
assert.isNotOk(element.shadowRoot
.querySelector('[data-action-key="' + key + '"]'));
assert.isFalse(element.$.moreActions.hidden);
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-gerrit.ts b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-gerrit.ts
index 85491ef..1d4bd06 100644
--- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-gerrit.ts
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-gerrit.ts
@@ -34,6 +34,7 @@
import {fontStyles} from '../../../styles/gr-font-styles';
import {formStyles} from '../../../styles/gr-form-styles';
import {menuPageStyles} from '../../../styles/gr-menu-page-styles';
+import {spinnerStyles} from '../../../styles/gr-spinner-styles';
import {subpageStyles} from '../../../styles/gr-subpage-styles';
import {tableStyles} from '../../../styles/gr-table-styles';
@@ -125,6 +126,7 @@
font: fontStyles,
form: formStyles,
menuPage: menuPageStyles,
+ spinner: spinnerStyles,
subPage: subpageStyles,
table: tableStyles,
};
diff --git a/polygerrit-ui/app/elements/shared/gr-label-info/gr-label-info.ts b/polygerrit-ui/app/elements/shared/gr-label-info/gr-label-info.ts
index 026ca4c..2df2ccb 100644
--- a/polygerrit-ui/app/elements/shared/gr-label-info/gr-label-info.ts
+++ b/polygerrit-ui/app/elements/shared/gr-label-info/gr-label-info.ts
@@ -22,23 +22,28 @@
import '../gr-button/gr-button';
import '../gr-icons/gr-icons';
import '../gr-label/gr-label';
+import '../gr-tooltip-content/gr-tooltip-content';
import {dom, EventApi} from '@polymer/polymer/lib/legacy/polymer.dom';
-import {PolymerElement} from '@polymer/polymer/polymer-element';
-import {htmlTemplate} from './gr-label-info_html';
-import {GerritNav} from '../../core/gr-navigation/gr-navigation';
-import {customElement, property} from '@polymer/decorators';
import {
- ChangeInfo,
AccountInfo,
LabelInfo,
ApprovalInfo,
AccountId,
isQuickLabelInfo,
isDetailedLabelInfo,
+ LabelNameToInfoMap,
} from '../../../types/common';
+import {LitElement, css, html} from 'lit';
+import {customElement, property} from 'lit/decorators';
import {GrButton} from '../gr-button/gr-button';
import {getVotingRangeOrDefault} from '../../../utils/label-util';
import {appContext} from '../../../services/app-context';
+import {ParsedChangeInfo} from '../../../types/types';
+import {fontStyles} from '../../../styles/gr-font-styles';
+import {sharedStyles} from '../../../styles/shared-styles';
+import {votingStyles} from '../../../styles/gr-voting-styles';
+import {ifDefined} from 'lit/directives/if-defined';
+import {fireReload} from '../../../utils/event-util';
declare global {
interface HTMLElementTagNameMap {
@@ -55,16 +60,12 @@
interface FormattedLabel {
className?: LabelClassName;
- account: ApprovalInfo;
+ account: ApprovalInfo | AccountInfo;
value: string;
}
@customElement('gr-label-info')
-export class GrLabelInfo extends PolymerElement {
- static get template() {
- return htmlTemplate;
- }
-
+export class GrLabelInfo extends LitElement {
@property({type: Object})
labelInfo?: LabelInfo;
@@ -72,7 +73,7 @@
label = '';
@property({type: Object})
- change?: ChangeInfo;
+ change?: ParsedChangeInfo;
@property({type: Object})
account?: AccountInfo;
@@ -87,11 +88,150 @@
// TODO(TS): not used, remove later
_xhrPromise?: Promise<void>;
+ static override get styles() {
+ return [
+ sharedStyles,
+ fontStyles,
+ votingStyles,
+ css`
+ .placeholder {
+ color: var(--deemphasized-text-color);
+ }
+ .hidden {
+ display: none;
+ }
+ /* Note that most of the .voteChip styles are coming from the
+ gr-voting-styles include. */
+ .voteChip {
+ display: flex;
+ justify-content: center;
+ margin-right: var(--spacing-s);
+ padding: 1px;
+ }
+ .max {
+ background-color: var(--vote-color-approved);
+ }
+ .min {
+ background-color: var(--vote-color-rejected);
+ }
+ .positive {
+ background-color: var(--vote-color-recommended);
+ border-radius: 12px;
+ border: 1px solid var(--vote-outline-recommended);
+ color: var(--chip-color);
+ }
+ .negative {
+ background-color: var(--vote-color-disliked);
+ border-radius: 12px;
+ border: 1px solid var(--vote-outline-disliked);
+ color: var(--chip-color);
+ }
+ .hidden {
+ display: none;
+ }
+ td {
+ vertical-align: top;
+ }
+ tr {
+ min-height: var(--line-height-normal);
+ }
+ gr-tooltip-content {
+ display: block;
+ }
+ gr-button {
+ vertical-align: top;
+ }
+ gr-button::part(paper-button) {
+ height: var(--line-height-normal);
+ width: var(--line-height-normal);
+ padding: 0;
+ }
+ gr-button[disabled] iron-icon {
+ color: var(--border-color);
+ }
+ gr-account-link {
+ --account-max-length: 100px;
+ margin-right: var(--spacing-xs);
+ }
+ iron-icon {
+ height: calc(var(--line-height-normal) - 2px);
+ width: calc(var(--line-height-normal) - 2px);
+ }
+ .labelValueContainer:not(:first-of-type) td {
+ padding-top: var(--spacing-s);
+ }
+ `,
+ ];
+ }
+
+ override render() {
+ return html` <p
+ class="placeholder ${this.computeShowPlaceholder(
+ this.labelInfo,
+ this.change?.labels
+ )}"
+ >
+ No votes
+ </p>
+ <table>
+ ${this.mapLabelInfo(
+ this.labelInfo,
+ this.account,
+ this.change?.labels
+ ).map(mappedLabel => this.renderLabel(mappedLabel))}
+ </table>`;
+ }
+
+ renderLabel(mappedLabel: FormattedLabel) {
+ const {labelInfo, change} = this;
+ return html` <tr class="labelValueContainer">
+ <td>
+ <gr-tooltip-content
+ has-tooltip
+ title="${this._computeValueTooltip(labelInfo, mappedLabel.value)}"
+ >
+ <gr-label class="${mappedLabel.className} voteChip font-small">
+ ${mappedLabel.value}
+ </gr-label>
+ </gr-tooltip-content>
+ </td>
+ <td>
+ <gr-account-link
+ .account="${mappedLabel.account}"
+ .change="${change}"
+ ></gr-account-link>
+ </td>
+ <td>
+ <gr-tooltip-content has-tooltip title="Remove vote">
+ <gr-button
+ link
+ aria-label="Remove vote"
+ @click="${this.onDeleteVote}"
+ data-account-id="${ifDefined(mappedLabel.account._account_id)}"
+ class="deleteBtn ${this.computeDeleteClass(
+ mappedLabel.account,
+ this.mutable,
+ change
+ )}"
+ >
+ <iron-icon icon="gr-icons:delete"></iron-icon>
+ </gr-button>
+ </gr-tooltip-content>
+ </td>
+ </tr>`;
+ }
+
/**
* This method also listens on change.labels.*,
* to trigger computation when a label is removed from the change.
+ *
+ * The third parameter is just for *triggering* computation.
*/
- _mapLabelInfo(labelInfo?: LabelInfo, account?: AccountInfo) {
+ private mapLabelInfo(
+ labelInfo?: LabelInfo,
+ account?: AccountInfo,
+ _?: LabelNameToInfoMap
+ ): FormattedLabel[] {
const result: FormattedLabel[] = [];
if (!labelInfo) {
return result;
@@ -106,7 +246,8 @@
{
value: ok ? '👍️' : '👎️',
className: ok ? LabelClassName.POSITIVE : LabelClassName.NEGATIVE,
- account: ok ? labelInfo.approved : labelInfo.rejected,
+ // executed only if approved or rejected is not undefined
+ account: ok ? labelInfo.approved! : labelInfo.rejected!,
},
];
}
@@ -141,7 +282,7 @@
labelClassName = LabelClassName.NEGATIVE;
}
}
- const formattedLabel = {
+ const formattedLabel: FormattedLabel = {
value: `${labelValPrefix}${label.value}`,
className: labelClassName,
account: label,
@@ -165,16 +306,16 @@
* @param reviewer An object describing the reviewer that left the
* vote.
*/
- _computeDeleteClass(
+ private computeDeleteClass(
reviewer: ApprovalInfo,
mutable: boolean,
- change: ChangeInfo
+ change?: ParsedChangeInfo
) {
if (!mutable || !change || !change.removable_reviewers) {
return 'hidden';
}
const removable = change.removable_reviewers;
- if (removable.find(r => r._account_id === reviewer._account_id)) {
+ if (removable.find(r => r._account_id === reviewer?._account_id)) {
return '';
}
return 'hidden';
@@ -184,7 +325,7 @@
* Closure annotation for Polymer.prototype.splice is off.
* For now, suppressing annotations.
*/
- _onDeleteVote(e: MouseEvent) {
+ private onDeleteVote(e: MouseEvent) {
if (!this.change) return;
e.preventDefault();
@@ -208,7 +349,7 @@
return;
}
if (this.change) {
- GerritNav.navigateToChange(this.change);
+ fireReload(this);
}
})
.catch(err => {
@@ -218,7 +359,7 @@
});
}
- _computeValueTooltip(labelInfo: LabelInfo, score: string) {
+ _computeValueTooltip(labelInfo: LabelInfo | undefined, score: string) {
if (
!labelInfo ||
!isDetailedLabelInfo(labelInfo) ||
@@ -232,8 +373,13 @@
/**
* This method also listens change.labels.* in
* order to trigger computation when a label is removed from the change.
+ *
+ * The second parameter is just for *triggering* computation.
*/
- _computeShowPlaceholder(labelInfo?: LabelInfo) {
+ private computeShowPlaceholder(
+ labelInfo?: LabelInfo,
+ _?: LabelNameToInfoMap
+ ) {
if (!labelInfo) {
return '';
}
diff --git a/polygerrit-ui/app/elements/shared/gr-label-info/gr-label-info_html.ts b/polygerrit-ui/app/elements/shared/gr-label-info/gr-label-info_html.ts
deleted file mode 100644
index f31b57f..0000000
--- a/polygerrit-ui/app/elements/shared/gr-label-info/gr-label-info_html.ts
+++ /dev/null
@@ -1,133 +0,0 @@
-/**
- * @license
- * Copyright (C) 2020 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 {html} from '@polymer/polymer/lib/utils/html-tag';
-
-export const htmlTemplate = html`
- <style include="gr-font-styles">
- /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
- </style>
- <style include="gr-voting-styles">
- /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
- </style>
- <style include="shared-styles">
- .placeholder {
- color: var(--deemphasized-text-color);
- }
- .hidden {
- display: none;
- }
- /* Note that most of the .voteChip styles are coming from the
- gr-voting-styles include. */
- .voteChip {
- display: flex;
- justify-content: center;
- margin-right: var(--spacing-s);
- padding: 1px;
- }
- .max {
- background-color: var(--vote-color-approved);
- }
- .min {
- background-color: var(--vote-color-rejected);
- }
- .positive {
- background-color: var(--vote-color-recommended);
- border-radius: 12px;
- border: 1px solid var(--vote-outline-recommended);
- color: var(--chip-color);
- }
- .negative {
- background-color: var(--vote-color-disliked);
- border-radius: 12px;
- border: 1px solid var(--vote-outline-disliked);
- color: var(--chip-color);
- }
- .hidden {
- display: none;
- }
- td {
- vertical-align: top;
- }
- tr {
- min-height: var(--line-height-normal);
- }
- gr-button {
- vertical-align: top;
- --gr-button: {
- height: var(--line-height-normal);
- width: var(--line-height-normal);
- padding: 0;
- }
- }
- gr-button[disabled] iron-icon {
- color: var(--border-color);
- }
- gr-account-link {
- --account-max-length: 100px;
- margin-right: var(--spacing-xs);
- }
- iron-icon {
- height: calc(var(--line-height-normal) - 2px);
- width: calc(var(--line-height-normal) - 2px);
- }
- .labelValueContainer:not(:first-of-type) td {
- padding-top: var(--spacing-s);
- }
- </style>
- <p
- class$="placeholder [[_computeShowPlaceholder(labelInfo, change.labels.*)]]"
- >
- No votes
- </p>
- <table>
- <template
- is="dom-repeat"
- items="[[_mapLabelInfo(labelInfo, account, change.labels.*)]]"
- as="mappedLabel"
- >
- <tr class="labelValueContainer">
- <td>
- <gr-label
- has-tooltip=""
- title="[[_computeValueTooltip(labelInfo, mappedLabel.value)]]"
- class$="[[mappedLabel.className]] voteChip font-small"
- >
- [[mappedLabel.value]]
- </gr-label>
- </td>
- <td>
- <gr-account-link
- account="[[mappedLabel.account]]"
- change="[[change]]"
- ></gr-account-link>
- </td>
- <td>
- <gr-button
- link=""
- aria-label="Remove vote"
- on-click="_onDeleteVote"
- tooltip="Remove vote"
- data-account-id$="[[mappedLabel.account._account_id]]"
- class$="deleteBtn [[_computeDeleteClass(mappedLabel.account, mutable, change)]]"
- >
- <iron-icon icon="gr-icons:delete"></iron-icon>
- </gr-button>
- </td>
- </tr>
- </template>
- </table>
-`;
diff --git a/polygerrit-ui/app/elements/shared/gr-label-info/gr-label-info_test.ts b/polygerrit-ui/app/elements/shared/gr-label-info/gr-label-info_test.ts
index b3235fa..cad1f69 100644
--- a/polygerrit-ui/app/elements/shared/gr-label-info/gr-label-info_test.ts
+++ b/polygerrit-ui/app/elements/shared/gr-label-info/gr-label-info_test.ts
@@ -31,7 +31,7 @@
import {GrAccountLink} from '../gr-account-link/gr-account-link';
import {
createAccountWithIdNameAndEmail,
- createChange,
+ createParsedChange,
} from '../../../test/test-data-generators';
import {LabelInfo} from '../../../types/common';
@@ -46,7 +46,7 @@
// Needed to trigger computed bindings.
element.account = {};
- element.change = {...createChange(), labels: {}};
+ element.change = {...createParsedChange(), labels: {}};
});
suite('remove reviewer votes', () => {
@@ -60,21 +60,23 @@
sinon.stub(element, '_computeValueTooltip').returns('');
element.account = account;
element.change = {
- ...createChange(),
+ ...createParsedChange(),
labels: {'Code-Review': label},
};
element.labelInfo = label;
element.label = 'Code-Review';
- await flush();
+ await element.updateComplete;
});
- test('_computeCanDeleteVote', () => {
+ test('_computeCanDeleteVote', async () => {
element.mutable = false;
+ await element.updateComplete;
const removeButton = queryAndAssert<GrButton>(element, 'gr-button');
assert.isTrue(isHidden(removeButton));
element.change!.removable_reviewers = [account];
element.mutable = true;
+ await element.updateComplete;
assert.isFalse(isHidden(removeButton));
});
@@ -109,14 +111,14 @@
suite('label color and order', () => {
test('valueless label rejected', async () => {
element.labelInfo = {rejected: {name: 'someone'}};
- await flush();
+ await element.updateComplete;
const labels = queryAll<GrLabel>(element, 'gr-label');
assert.isTrue(labels[0].classList.contains('negative'));
});
test('valueless label approved', async () => {
element.labelInfo = {approved: {name: 'someone'}};
- await flush();
+ await element.updateComplete;
const labels = queryAll<GrLabel>(element, 'gr-label');
assert.isTrue(labels[0].classList.contains('positive'));
});
@@ -137,7 +139,7 @@
'+2': 'Ready to submit',
},
};
- await flush();
+ await element.updateComplete;
const labels = queryAll<GrLabel>(element, 'gr-label');
assert.isTrue(labels[0].classList.contains('max'));
assert.isTrue(labels[1].classList.contains('positive'));
@@ -157,7 +159,7 @@
'+1': 'Looks good to me',
},
};
- await flush();
+ await element.updateComplete;
const labels = queryAll<GrLabel>(element, 'gr-label');
assert.isTrue(labels[0].classList.contains('max'));
assert.isTrue(labels[1].classList.contains('min'));
@@ -175,7 +177,7 @@
'+2': 'Looks good to me',
},
};
- await flush();
+ await element.updateComplete;
const labels = queryAll<GrLabel>(element, 'gr-label');
assert.isTrue(labels[0].classList.contains('max'));
assert.isTrue(labels[1].classList.contains('positive'));
@@ -195,7 +197,7 @@
'+1': 'Looks good to me',
},
};
- await flush();
+ await element.updateComplete;
const chips = queryAll<GrAccountLink>(element, 'gr-account-link');
assert.equal(chips[0].account!._account_id, element.account._account_id);
});
@@ -217,7 +219,7 @@
assert.equal(element._computeValueTooltip(labelInfo, score), '');
});
- test('placeholder', () => {
+ test('placeholder', async () => {
const values = {
'0': 'No score',
'+1': 'good',
@@ -226,30 +228,37 @@
'-2': 'terrible',
};
element.labelInfo = {};
+ await element.updateComplete;
assert.isFalse(
isHidden(queryAndAssert<HTMLParagraphElement>(element, '.placeholder'))
);
element.labelInfo = {all: [], values};
+ await element.updateComplete;
assert.isFalse(
isHidden(queryAndAssert<HTMLParagraphElement>(element, '.placeholder'))
);
element.labelInfo = {all: [{value: 1}], values};
+ await element.updateComplete;
assert.isTrue(
isHidden(queryAndAssert<HTMLParagraphElement>(element, '.placeholder'))
);
element.labelInfo = {rejected: account};
+ await element.updateComplete;
assert.isTrue(
isHidden(queryAndAssert<HTMLParagraphElement>(element, '.placeholder'))
);
element.labelInfo = {rejected: account, all: [{value: 1}], values};
+ await element.updateComplete;
assert.isTrue(
isHidden(queryAndAssert<HTMLParagraphElement>(element, '.placeholder'))
);
element.labelInfo = {approved: account};
+ await element.updateComplete;
assert.isTrue(
isHidden(queryAndAssert<HTMLParagraphElement>(element, '.placeholder'))
);
element.labelInfo = {approved: account, all: [{value: 1}], values};
+ await element.updateComplete;
assert.isTrue(
isHidden(queryAndAssert<HTMLParagraphElement>(element, '.placeholder'))
);
diff --git a/polygerrit-ui/app/elements/shared/gr-label/gr-label.ts b/polygerrit-ui/app/elements/shared/gr-label/gr-label.ts
index fd10145..842b35e 100644
--- a/polygerrit-ui/app/elements/shared/gr-label/gr-label.ts
+++ b/polygerrit-ui/app/elements/shared/gr-label/gr-label.ts
@@ -21,10 +21,8 @@
* used in gr-label-info.
*/
-import {PolymerElement} from '@polymer/polymer/polymer-element';
-import {customElement} from '@polymer/decorators';
-import {htmlTemplate} from './gr-label_html';
-import {TooltipMixin} from '../../../mixins/gr-tooltip-mixin/gr-tooltip-mixin';
+import {html, LitElement} from 'lit';
+import {customElement} from 'lit/decorators';
declare global {
interface HTMLElementTagNameMap {
@@ -32,12 +30,13 @@
}
}
-// This avoids JSC_DYNAMIC_EXTENDS_WITHOUT_JSDOC closure compiler error.
-const base = TooltipMixin(PolymerElement);
-
@customElement('gr-label')
-export class GrLabel extends base {
- static get template() {
- return htmlTemplate;
+export class GrLabel extends LitElement {
+ static override get styles() {
+ return [];
+ }
+
+ override render() {
+ return html` <slot></slot> `;
}
}
diff --git a/polygerrit-ui/app/elements/shared/gr-limited-text/gr-limited-text.ts b/polygerrit-ui/app/elements/shared/gr-limited-text/gr-limited-text.ts
index f5a0c4d..7008db2 100644
--- a/polygerrit-ui/app/elements/shared/gr-limited-text/gr-limited-text.ts
+++ b/polygerrit-ui/app/elements/shared/gr-limited-text/gr-limited-text.ts
@@ -14,10 +14,8 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-import {PolymerElement} from '@polymer/polymer/polymer-element';
-import {htmlTemplate} from './gr-limited-text_html';
-import {TooltipMixin} from '../../../mixins/gr-tooltip-mixin/gr-tooltip-mixin';
-import {customElement, observe, property} from '@polymer/decorators';
+import {customElement, property} from 'lit/decorators';
+import {html, LitElement} from 'lit';
declare global {
interface HTMLElementTagNameMap {
@@ -25,9 +23,6 @@
}
}
-// This avoids JSC_DYNAMIC_EXTENDS_WITHOUT_JSDOC closure compiler error.
-const base = TooltipMixin(PolymerElement);
-
/**
* The gr-limited-text element is for displaying text with a maximum length
* (in number of characters) to display. If the length of the text exceeds the
@@ -35,57 +30,56 @@
* and a tooltip containing the full text is enabled.
*/
@customElement('gr-limited-text')
-export class GrLimitedText extends base {
- static get template() {
- return htmlTemplate;
- }
-
+export class GrLimitedText extends LitElement {
/** The un-truncated text to display. */
@property({type: String})
- text?: string;
+ text = '';
/** The maximum length for the text to display before truncating. */
@property({type: Number})
- limit?: number;
+ limit = 0;
@property({type: String})
tooltip?: string;
- /** Boolean property used by TooltipMixin. */
- @property({type: Boolean})
- override hasTooltip = false;
+ static override get styles() {
+ return [];
+ }
- /** Boolean property used by TooltipMixin. */
- @property({type: Boolean})
- disableTooltip = false;
-
- /**
- * The text or limit have changed. Recompute whether a tooltip needs to be
- * enabled.
- */
- @observe('text', 'tooltip', 'limit')
- _updateTitle(text?: string, tooltip?: string, limit?: number) {
- text = text ?? '';
- tooltip = tooltip ?? '';
- limit = limit ?? 0;
-
- this.hasTooltip = !!tooltip || (!!limit && text.length > limit);
- if (this.hasTooltip && !this.disableTooltip) {
- // Combine the text and title if over-length
- if (limit && text.length > limit) {
- this.title = `${text}${tooltip ? ` (${tooltip})` : ''}`;
- } else {
- this.title = tooltip;
- }
+ override render() {
+ if (this.tooltip || this.tooLong()) {
+ return html` <gr-tooltip-content
+ has-tooltip
+ .title=${this.renderTooltip()}
+ >
+ ${this.renderText()}
+ </gr-tooltip-content>`;
} else {
- this.title = '';
+ return this.renderText();
}
}
- _computeDisplayText(text?: string, limit?: number) {
- if (!!limit && !!text && text.length > limit) {
- return text.substr(0, limit - 1) + '…';
+ // Should be private but used in tests.
+ renderText() {
+ if (this.tooLong()) {
+ return this.text.substr(0, this.limit - 1) + '…';
}
- return text;
+ return this.text;
+ }
+
+ private renderTooltip() {
+ if (this.tooLong()) {
+ return `${this.text}${this.tooltip ? ` (${this.tooltip})` : ''}`;
+ } else if (this.tooltip) {
+ return this.tooltip;
+ } else {
+ return '';
+ }
+ }
+
+ private tooLong() {
+ if (!this.limit) return false;
+ if (!this.text) return false;
+ return this.text.length > this.limit;
}
}
diff --git a/polygerrit-ui/app/elements/shared/gr-limited-text/gr-limited-text_html.ts b/polygerrit-ui/app/elements/shared/gr-limited-text/gr-limited-text_html.ts
deleted file mode 100644
index b942d07..0000000
--- a/polygerrit-ui/app/elements/shared/gr-limited-text/gr-limited-text_html.ts
+++ /dev/null
@@ -1,19 +0,0 @@
-/**
- * @license
- * Copyright (C) 2020 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 {html} from '@polymer/polymer/lib/utils/html-tag';
-
-export const htmlTemplate = html` [[_computeDisplayText(text, limit)]] `;
diff --git a/polygerrit-ui/app/elements/shared/gr-limited-text/gr-limited-text_test.js b/polygerrit-ui/app/elements/shared/gr-limited-text/gr-limited-text_test.js
index 3b99d6d..e3e72d0 100644
--- a/polygerrit-ui/app/elements/shared/gr-limited-text/gr-limited-text_test.js
+++ b/polygerrit-ui/app/elements/shared/gr-limited-text/gr-limited-text_test.js
@@ -23,77 +23,65 @@
suite('gr-limited-text tests', () => {
let element;
- setup(() => {
+ setup(async () => {
element = basicFixture.instantiate();
+ await element.updateComplete;
});
- test('tooltip without title input', () => {
- const updateSpy = sinon.spy(element, '_updateTitle');
+ test('tooltip without title input', async () => {
element.text = 'abc 123';
- flush();
- assert.isTrue(updateSpy.calledOnce);
- assert.isNotOk(element.getAttribute('title'));
- assert.isFalse(element.hasTooltip);
+ await element.updateComplete;
+ assert.isNotOk(element.shadowRoot.querySelector('gr-tooltip-content'));
element.limit = 10;
- flush();
- assert.isTrue(updateSpy.calledTwice);
- assert.isNotOk(element.getAttribute('title'));
- assert.isFalse(element.hasTooltip);
+ await element.updateComplete;
+ assert.isNotOk(element.shadowRoot.querySelector('gr-tooltip-content'));
element.limit = 3;
- flush();
- assert.equal(updateSpy.callCount, 3);
- assert.equal(element.getAttribute('title'), 'abc 123');
- assert.equal(element.title, 'abc 123');
- assert.isTrue(element.hasTooltip);
+ await element.updateComplete;
+ assert.isOk(element.shadowRoot.querySelector('gr-tooltip-content'));
+ assert.equal(
+ element.shadowRoot.querySelector('gr-tooltip-content').title,
+ 'abc 123');
element.limit = 100;
- flush();
- assert.equal(updateSpy.callCount, 4);
- assert.isFalse(element.hasTooltip);
+ await element.updateComplete;
+ assert.isNotOk(element.shadowRoot.querySelector('gr-tooltip-content'));
element.limit = null;
- flush();
- assert.equal(updateSpy.callCount, 5);
- assert.isNotOk(element.getAttribute('title'));
- assert.isFalse(element.hasTooltip);
+ await element.updateComplete;
+ assert.isNotOk(element.shadowRoot.querySelector('gr-tooltip-content'));
});
- test('with tooltip input', () => {
- const updateSpy = sinon.spy(element, '_updateTitle');
+ test('with tooltip input', async () => {
element.tooltip = 'abc 123';
- flush();
- assert.isTrue(updateSpy.calledOnce);
- assert.isTrue(element.hasTooltip);
- assert.equal(element.getAttribute('title'), 'abc 123');
- assert.equal(element.title, 'abc 123');
+ await element.updateComplete;
+ let tooltipContent = element.shadowRoot.querySelector('gr-tooltip-content');
+ assert.isOk(tooltipContent);
+ assert.equal(tooltipContent.title, 'abc 123');
element.text = 'abc';
- flush();
- assert.equal(element.getAttribute('title'), 'abc 123');
- assert.isTrue(element.hasTooltip);
+ await element.updateComplete;
+ tooltipContent = element.shadowRoot.querySelector('gr-tooltip-content');
+ assert.isOk(tooltipContent);
+ assert.equal(tooltipContent.title, 'abc 123');
element.text = 'abcdef';
element.limit = 3;
- flush();
- assert.equal(element.getAttribute('title'), 'abcdef (abc 123)');
- assert.isTrue(element.hasTooltip);
+ await element.updateComplete;
+ tooltipContent = element.shadowRoot.querySelector('gr-tooltip-content');
+ assert.isOk(tooltipContent);
+ assert.equal(tooltipContent.title, 'abcdef (abc 123)');
});
test('_computeDisplayText', () => {
- assert.equal(element._computeDisplayText('foo bar', 100), 'foo bar');
- assert.equal(element._computeDisplayText('foo bar', 4), 'foo…');
- assert.equal(element._computeDisplayText('foo bar', null), 'foo bar');
- });
-
- test('when disable tooltip', () => {
- sinon.spy(element, '_updateTitle');
- element.text = 'abcdefghijklmn';
- element.disableTooltip = true;
- element.limit = 10;
- flush();
- assert.equal(element.getAttribute('title'), '');
+ element.text = 'foo bar';
+ element.limit = 100;
+ assert.equal(element.renderText(), 'foo bar');
+ element.limit = 4;
+ assert.equal(element.renderText(), 'foo…');
+ element.limit = 0;
+ assert.equal(element.renderText(), 'foo bar');
});
});
diff --git a/polygerrit-ui/app/elements/shared/gr-linked-chip/gr-linked-chip.ts b/polygerrit-ui/app/elements/shared/gr-linked-chip/gr-linked-chip.ts
index 5bf5a8f..801b8bf 100644
--- a/polygerrit-ui/app/elements/shared/gr-linked-chip/gr-linked-chip.ts
+++ b/polygerrit-ui/app/elements/shared/gr-linked-chip/gr-linked-chip.ts
@@ -50,7 +50,7 @@
@property({type: Number})
limit?: number;
- static get styles() {
+ static override get styles() {
return [
sharedStyles,
css`
@@ -84,9 +84,10 @@
];
}
- render() {
+ override render() {
// To pass CSS mixins for @apply to Polymer components, they need to appear
// in <style> inside the template.
+ /* eslint-disable lit/prefer-static-styles */
const customStyle = html`
<style>
gr-button::part(paper-button),
diff --git a/polygerrit-ui/app/elements/shared/gr-list-view/gr-list-view.ts b/polygerrit-ui/app/elements/shared/gr-list-view/gr-list-view.ts
index 055c45f..4f02897 100644
--- a/polygerrit-ui/app/elements/shared/gr-list-view/gr-list-view.ts
+++ b/polygerrit-ui/app/elements/shared/gr-list-view/gr-list-view.ts
@@ -53,7 +53,7 @@
filter?: string;
@property({type: Number})
- offset?: number;
+ offset = 0;
@property({type: Boolean})
loading?: boolean;
@@ -102,8 +102,8 @@
offset: number,
direction: number,
itemsPerPage: number,
- filter: string,
- path: string
+ filter: string | undefined,
+ path = ''
) {
// Offset could be a string when passed from the router.
offset = +(offset || 0);
diff --git a/polygerrit-ui/app/elements/shared/gr-overlay/gr-overlay.ts b/polygerrit-ui/app/elements/shared/gr-overlay/gr-overlay.ts
index 1622365..3d6b5ed 100644
--- a/polygerrit-ui/app/elements/shared/gr-overlay/gr-overlay.ts
+++ b/polygerrit-ui/app/elements/shared/gr-overlay/gr-overlay.ts
@@ -22,7 +22,7 @@
import {IronOverlayBehavior} from '@polymer/iron-overlay-behavior/iron-overlay-behavior';
import {findActiveElement} from '../../../utils/dom-util';
import {fireEvent} from '../../../utils/event-util';
-import {getHovercardContainer} from '../gr-hovercard/gr-hovercard-behavior';
+import {getHovercardContainer} from '../../../mixins/hovercard-mixin/hovercard-mixin';
const AWAIT_MAX_ITERS = 10;
const AWAIT_STEP = 5;
@@ -40,6 +40,9 @@
IronOverlayBehavior as IronOverlayBehavior
);
+/**
+ * @attr {Boolean} with-backdrop - inherited from IronOverlay
+ */
@customElement('gr-overlay')
export class GrOverlay extends base {
static get template() {
diff --git a/polygerrit-ui/app/elements/shared/gr-page-nav/gr-page-nav.ts b/polygerrit-ui/app/elements/shared/gr-page-nav/gr-page-nav.ts
index b2477dd..423a1a8 100644
--- a/polygerrit-ui/app/elements/shared/gr-page-nav/gr-page-nav.ts
+++ b/polygerrit-ui/app/elements/shared/gr-page-nav/gr-page-nav.ts
@@ -53,7 +53,7 @@
this.bodyScrollHandler = () => this._handleBodyScroll();
}
- connectedCallback() {
+ override connectedCallback() {
super.connectedCallback();
window.addEventListener('scroll', this.bodyScrollHandler);
}
diff --git a/polygerrit-ui/app/elements/shared/gr-select/gr-select.ts b/polygerrit-ui/app/elements/shared/gr-select/gr-select.ts
index e71add9..571272d 100644
--- a/polygerrit-ui/app/elements/shared/gr-select/gr-select.ts
+++ b/polygerrit-ui/app/elements/shared/gr-select/gr-select.ts
@@ -34,7 +34,7 @@
}
@property({type: String, notify: true})
- bindValue?: string;
+ bindValue?: string | number;
get nativeSelect() {
// gr-select is not a shadow component
@@ -49,14 +49,12 @@
// It's possible to have a value of 0.
if (this.bindValue !== undefined) {
// Set for chrome/safari so it happens instantly
- this.nativeSelect.value = this.bindValue;
+ this.nativeSelect.value = String(this.bindValue);
// Async needed for firefox to populate value. It was trying to do it
// before options from a dom-repeat were rendered previously.
// See https://bugs.chromium.org/p/gerrit/issues/detail?id=7735
setTimeout(() => {
- // TODO(TS): maybe should check for undefined before assigning
- // or fallback to ''
- this.nativeSelect.value = this.bindValue!;
+ this.nativeSelect.value = String(this.bindValue);
}, 1);
}
}
diff --git a/polygerrit-ui/app/elements/shared/gr-textarea/gr-textarea.ts b/polygerrit-ui/app/elements/shared/gr-textarea/gr-textarea.ts
index ce1eec3..434da1f 100644
--- a/polygerrit-ui/app/elements/shared/gr-textarea/gr-textarea.ts
+++ b/polygerrit-ui/app/elements/shared/gr-textarea/gr-textarea.ts
@@ -33,7 +33,7 @@
Item,
ItemSelectedEvent,
} from '../gr-autocomplete-dropdown/gr-autocomplete-dropdown';
-import {CustomKeyboardEvent} from '../../../types/events';
+import {IronKeyboardEvent} from '../../../types/events';
const MAX_ITEMS_DROPDOWN = 10;
@@ -238,7 +238,7 @@
this._setEmoji(this.$.emojiSuggestions.getCurrentText());
}
- _handleEnterByKey(e: CustomKeyboardEvent) {
+ _handleEnterByKey(e: IronKeyboardEvent) {
// Enter should have newline behavior if the picker is closed or if the user
// has only typed ':'. Also make sure that shortcuts aren't clobbered.
if (this._hideEmojiAutocomplete || this.disableEnterKeyForSelectingEmoji) {
@@ -420,7 +420,7 @@
);
}
- private indent(e: CustomKeyboardEvent): void {
+ private indent(e: IronKeyboardEvent): void {
if (!document.queryCommandSupported('insertText')) {
return;
}
diff --git a/polygerrit-ui/app/elements/shared/gr-textarea/gr-textarea_test.ts b/polygerrit-ui/app/elements/shared/gr-textarea/gr-textarea_test.ts
index 7790c73..7e59692 100644
--- a/polygerrit-ui/app/elements/shared/gr-textarea/gr-textarea_test.ts
+++ b/polygerrit-ui/app/elements/shared/gr-textarea/gr-textarea_test.ts
@@ -20,7 +20,7 @@
import {GrTextarea} from './gr-textarea';
import {html} from '@polymer/polymer/lib/utils/html-tag';
import * as MockInteractions from '@polymer/iron-test-helpers/mock-interactions';
-import {CustomKeyboardEvent} from '../../../types/events';
+import {IronKeyboardEvent} from '../../../types/events';
import {ItemSelectedEvent} from '../gr-autocomplete-dropdown/gr-autocomplete-dropdown';
const basicFixture = fixtureFromElement('gr-textarea');
@@ -240,7 +240,7 @@
element._handleEnterByKey(
new CustomEvent('keydown', {
detail: {keyboardEvent: {keyCode: 13}},
- }) as CustomKeyboardEvent
+ }) as IronKeyboardEvent
);
await flush();
assert.deepEqual(indentCommand.args[0], ['insertText', false, '\n ']);
@@ -252,7 +252,7 @@
element._handleEnterByKey(
new CustomEvent('keydown', {
detail: {keyboardEvent: {keyCode: 13, ctrlKey: true}},
- }) as CustomKeyboardEvent
+ }) as IronKeyboardEvent
);
await flush();
assert.isTrue(indentCommand.notCalled);
@@ -260,7 +260,7 @@
element._handleEnterByKey(
new CustomEvent('keydown', {
detail: {keyboardEvent: {keyCode: 13, metaKey: true}},
- }) as CustomKeyboardEvent
+ }) as IronKeyboardEvent
);
await flush();
assert.isTrue(indentCommand.notCalled);
diff --git a/polygerrit-ui/app/elements/shared/gr-tooltip-content/gr-tooltip-content.ts b/polygerrit-ui/app/elements/shared/gr-tooltip-content/gr-tooltip-content.ts
index 9d228ab..0585aec8 100644
--- a/polygerrit-ui/app/elements/shared/gr-tooltip-content/gr-tooltip-content.ts
+++ b/polygerrit-ui/app/elements/shared/gr-tooltip-content/gr-tooltip-content.ts
@@ -15,10 +15,13 @@
* limitations under the License.
*/
import '../gr-icons/gr-icons';
-import {PolymerElement} from '@polymer/polymer/polymer-element';
-import {htmlTemplate} from './gr-tooltip-content_html';
-import {TooltipMixin} from '../../../mixins/gr-tooltip-mixin/gr-tooltip-mixin';
-import {customElement, property} from '@polymer/decorators';
+import '../gr-tooltip/gr-tooltip';
+import {getRootElement} from '../../../scripts/rootElement';
+import {GrTooltip} from '../gr-tooltip/gr-tooltip';
+import {css, html, LitElement, PropertyValues} from 'lit';
+import {customElement, property, state} from 'lit/decorators';
+
+const BOTTOM_OFFSET = 7.2; // Height of the arrow in tooltip.
declare global {
interface HTMLElementTagNameMap {
@@ -26,21 +29,202 @@
}
}
-// This avoids JSC_DYNAMIC_EXTENDS_WITHOUT_JSDOC closure compiler error.
-const base = TooltipMixin(PolymerElement);
-
-/**
- * Transclude anything inside and wrap them to support tooltip functionality.
- */
@customElement('gr-tooltip-content')
-export class GrTooltipContent extends base {
- static get template() {
- return htmlTemplate;
- }
+export class GrTooltipContent extends LitElement {
+ @property({type: Boolean, attribute: 'has-tooltip', reflect: true})
+ hasTooltip = false;
- @property({type: String, reflectToAttribute: true})
+ @property({type: Boolean, attribute: 'position-below', reflect: true})
+ positionBelow = false;
+
+ @property({type: String, attribute: 'max-width', reflect: true})
maxWidth?: string;
- @property({type: Boolean})
+ @property({type: Boolean, attribute: 'show-icon'})
showIcon = false;
+
+ // Should be private but used in tests.
+ @state()
+ isTouchDevice = 'ontouchstart' in document.documentElement;
+
+ // Should be private but used in tests.
+ tooltip: GrTooltip | null = null;
+
+ @state()
+ private originalTitle = '';
+
+ private hasSetupTooltipListeners = false;
+
+ private readonly windowScrollHandler: () => void;
+
+ private readonly showHandler: () => void;
+
+ private readonly hideHandler: (e: Event) => void;
+
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars, @typescript-eslint/no-explicit-any
+ constructor() {
+ super();
+ this.windowScrollHandler = () => this._handleWindowScroll();
+ this.showHandler = () => this._handleShowTooltip();
+ this.hideHandler = (e: Event | undefined) => this._handleHideTooltip(e);
+ }
+
+ override disconnectedCallback() {
+ this._handleHideTooltip(undefined);
+ this.removeEventListener('mouseenter', this.showHandler);
+ window.removeEventListener('scroll', this.windowScrollHandler);
+ super.disconnectedCallback();
+ }
+
+ static override get styles() {
+ return [
+ css`
+ iron-icon {
+ width: var(--line-height-normal);
+ height: var(--line-height-normal);
+ vertical-align: top;
+ }
+ `,
+ ];
+ }
+
+ override render() {
+ return html`
+ <slot></slot>
+ ${this.renderIcon()}
+ `;
+ }
+
+ renderIcon() {
+ if (!this.showIcon) return;
+ return html`<iron-icon icon="gr-icons:info"></iron-icon>`;
+ }
+
+ override updated(changedProperties: PropertyValues) {
+ if (changedProperties.has('hasTooltip')) {
+ this.setupTooltipListeners();
+ }
+ }
+
+ private setupTooltipListeners() {
+ if (!this.hasTooltip) {
+ if (this.hasSetupTooltipListeners) {
+ // if attribute set to false, remove the listener
+ this.removeEventListener('mouseenter', this.showHandler);
+ this.hasSetupTooltipListeners = false;
+ }
+ return;
+ }
+
+ if (this.hasSetupTooltipListeners) {
+ return;
+ }
+ this.hasSetupTooltipListeners = true;
+ this.addEventListener('mouseenter', this.showHandler);
+ }
+
+ _handleShowTooltip() {
+ if (this.isTouchDevice) {
+ return;
+ }
+
+ if (
+ !this.hasAttribute('title') ||
+ this.getAttribute('title') === '' ||
+ this.tooltip
+ ) {
+ return;
+ }
+
+ // Store the title attribute text then set it to an empty string to
+ // prevent it from showing natively.
+ this.originalTitle = this.getAttribute('title') || '';
+ this.setAttribute('title', '');
+
+ const tooltip = document.createElement('gr-tooltip');
+ tooltip.text = this.originalTitle;
+ tooltip.maxWidth = this.getAttribute('max-width') || '';
+ tooltip.positionBelow = this.hasAttribute('position-below');
+
+ // Set visibility to hidden before appending to the DOM so that
+ // calculations can be made based on the element’s size.
+ tooltip.style.visibility = 'hidden';
+ getRootElement().appendChild(tooltip);
+ this._positionTooltip(tooltip);
+ tooltip.style.visibility = 'initial';
+
+ this.tooltip = tooltip;
+ window.addEventListener('scroll', this.windowScrollHandler);
+ this.addEventListener('mouseleave', this.hideHandler);
+ this.addEventListener('click', this.hideHandler);
+ tooltip.addEventListener('mouseleave', this.hideHandler);
+ }
+
+ _handleHideTooltip(e: Event | undefined) {
+ if (this.isTouchDevice) {
+ return;
+ }
+ if (!this.hasAttribute('title') || !this.originalTitle) {
+ return;
+ }
+ // Do not hide if mouse left this or this.tooltip and came to this or
+ // this.tooltip
+ if (
+ (e as MouseEvent)?.relatedTarget === this.tooltip ||
+ (e as MouseEvent)?.relatedTarget === this
+ ) {
+ return;
+ }
+
+ window.removeEventListener('scroll', this.windowScrollHandler);
+ this.removeEventListener('mouseleave', this.hideHandler);
+ this.removeEventListener('click', this.hideHandler);
+ this.setAttribute('title', this.originalTitle);
+ this.tooltip?.removeEventListener('mouseleave', this.hideHandler);
+
+ if (this.tooltip?.parentNode) {
+ this.tooltip.parentNode.removeChild(this.tooltip);
+ }
+ this.tooltip = null;
+ }
+
+ _handleWindowScroll() {
+ if (!this.tooltip) {
+ return;
+ }
+ // This wait is needed for tooltips to be positioned correctly in Firefox
+ // and Safari.
+ this.updateComplete.then(() => this._positionTooltip(this.tooltip));
+ }
+
+ // private but used in tests.
+ async _positionTooltip(tooltip: GrTooltip | null) {
+ if (tooltip === null) return;
+ const rect = this.getBoundingClientRect();
+ const boxRect = tooltip.getBoundingClientRect();
+ if (!tooltip.parentElement) {
+ return;
+ }
+ const parentRect = tooltip.parentElement.getBoundingClientRect();
+ const top = rect.top - parentRect.top;
+ const left = rect.left - parentRect.left + (rect.width - boxRect.width) / 2;
+ const right = parentRect.width - left - boxRect.width;
+ if (left < 0) {
+ tooltip.updateStyles({
+ '--gr-tooltip-arrow-center-offset': `${left}px`,
+ });
+ } else if (right < 0) {
+ tooltip.updateStyles({
+ '--gr-tooltip-arrow-center-offset': `${-0.5 * right}px`,
+ });
+ }
+ tooltip.style.left = `${Math.max(0, left)}px`;
+
+ if (!this.positionBelow) {
+ tooltip.style.top = `${Math.max(0, top)}px`;
+ tooltip.style.transform = `translateY(calc(-100% - ${BOTTOM_OFFSET}px))`;
+ } else {
+ tooltip.style.top = `${top + rect.height + BOTTOM_OFFSET}px`;
+ }
+ }
}
diff --git a/polygerrit-ui/app/elements/shared/gr-tooltip-content/gr-tooltip-content_html.ts b/polygerrit-ui/app/elements/shared/gr-tooltip-content/gr-tooltip-content_html.ts
deleted file mode 100644
index 952420d..0000000
--- a/polygerrit-ui/app/elements/shared/gr-tooltip-content/gr-tooltip-content_html.ts
+++ /dev/null
@@ -1,30 +0,0 @@
-/**
- * @license
- * Copyright (C) 2020 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 {html} from '@polymer/polymer/lib/utils/html-tag';
-
-export const htmlTemplate = html`
- <style>
- iron-icon {
- width: var(--line-height-normal);
- height: var(--line-height-normal);
- vertical-align: top;
- }
- </style>
- <slot></slot
- ><!--
- --><iron-icon icon="gr-icons:info" hidden$="[[!showIcon]]"></iron-icon>
-`;
diff --git a/polygerrit-ui/app/elements/shared/gr-tooltip-content/gr-tooltip-content_test.js b/polygerrit-ui/app/elements/shared/gr-tooltip-content/gr-tooltip-content_test.js
index f905eaa..8d3bbb0 100644
--- a/polygerrit-ui/app/elements/shared/gr-tooltip-content/gr-tooltip-content_test.js
+++ b/polygerrit-ui/app/elements/shared/gr-tooltip-content/gr-tooltip-content_test.js
@@ -17,35 +17,162 @@
import '../../../test/common-test-setup-karma.js';
import './gr-tooltip-content.js';
-import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js';
-import {html} from '@polymer/polymer/lib/utils/html-tag.js';
-const basicFixture = fixtureFromTemplate(html`
-<gr-tooltip-content>
- </gr-tooltip-content>
-`);
+const basicFixture = fixtureFromElement('gr-tooltip-content');
suite('gr-tooltip-content tests', () => {
let element;
- setup(() => {
+
+ function makeTooltip(tooltipRect, parentRect) {
+ return {
+ getBoundingClientRect() { return tooltipRect; },
+ updateStyles: sinon.stub(),
+ style: {left: 0, top: 0},
+ parentElement: {
+ getBoundingClientRect() { return parentRect; },
+ },
+ };
+ }
+
+ setup(async () => {
element = basicFixture.instantiate();
+ element.title = 'title';
+ await element.updateComplete;
});
test('icon is not visible by default', () => {
- assert.equal(dom(element.root)
- .querySelector('iron-icon').hidden, true);
+ assert.isNotOk(element.shadowRoot.querySelector('iron-icon'));
});
- test('position-below attribute is reflected', () => {
+ test('icon is visible with showIcon property', async () => {
+ element.showIcon = true;
+ await element.updateComplete;
+ assert.isOk(element.shadowRoot.querySelector('iron-icon'));
+ });
+
+ test('position-below attribute is reflected', async () => {
assert.isFalse(element.hasAttribute('position-below'));
element.positionBelow = true;
+ await element.updateComplete;
assert.isTrue(element.hasAttribute('position-below'));
});
- test('icon is visible with showIcon property', () => {
- element.showIcon = true;
- assert.equal(dom(element.root)
- .querySelector('iron-icon').hidden, false);
+ test('normal position', () => {
+ sinon.stub(element, 'getBoundingClientRect').callsFake(() => {
+ return {top: 100, left: 100, width: 200};
+ });
+ const tooltip = makeTooltip(
+ {height: 30, width: 50},
+ {top: 0, left: 0, width: 1000});
+
+ element._positionTooltip(tooltip);
+ assert.isFalse(tooltip.updateStyles.called);
+ assert.equal(tooltip.style.left, '175px');
+ assert.equal(tooltip.style.top, '100px');
+ });
+
+ test('left side position', () => {
+ sinon.stub(element, 'getBoundingClientRect').callsFake(() => {
+ return {top: 100, left: 10, width: 50};
+ });
+ const tooltip = makeTooltip(
+ {height: 30, width: 120},
+ {top: 0, left: 0, width: 1000});
+
+ element._positionTooltip(tooltip);
+ assert.isTrue(tooltip.updateStyles.called);
+ const offset = tooltip.updateStyles
+ .lastCall.args[0]['--gr-tooltip-arrow-center-offset'];
+ assert.isBelow(parseFloat(offset.replace(/px$/, '')), 0);
+ assert.equal(tooltip.style.left, '0px');
+ assert.equal(tooltip.style.top, '100px');
+ });
+
+ test('right side position', () => {
+ sinon.stub(element, 'getBoundingClientRect').callsFake(() => {
+ return {top: 100, left: 950, width: 50};
+ });
+ const tooltip = makeTooltip(
+ {height: 30, width: 120},
+ {top: 0, left: 0, width: 1000});
+
+ element._positionTooltip(tooltip);
+ assert.isTrue(tooltip.updateStyles.called);
+ const offset = tooltip.updateStyles
+ .lastCall.args[0]['--gr-tooltip-arrow-center-offset'];
+ assert.isAbove(parseFloat(offset.replace(/px$/, '')), 0);
+ assert.equal(tooltip.style.left, '915px');
+ assert.equal(tooltip.style.top, '100px');
+ });
+
+ test('position to bottom', () => {
+ sinon.stub(element, 'getBoundingClientRect').callsFake(() => {
+ return {top: 100, left: 950, width: 50, height: 50};
+ });
+ const tooltip = makeTooltip(
+ {height: 30, width: 120},
+ {top: 0, left: 0, width: 1000});
+
+ element.positionBelow = true;
+ element._positionTooltip(tooltip);
+ assert.isTrue(tooltip.updateStyles.called);
+ const offset = tooltip.updateStyles
+ .lastCall.args[0]['--gr-tooltip-arrow-center-offset'];
+ assert.isAbove(parseFloat(offset.replace(/px$/, '')), 0);
+ assert.equal(tooltip.style.left, '915px');
+ assert.equal(tooltip.style.top, '157.2px');
+ });
+
+ test('hides tooltip when detached', async () => {
+ sinon.stub(element, '_handleHideTooltip');
+ element.remove();
+ await element.updateComplete;
+ assert.isTrue(element._handleHideTooltip.called);
+ });
+
+ test('sets up listeners when has-tooltip is changed', async () => {
+ const addListenerStub = sinon.stub(element, 'addEventListener');
+ element.hasTooltip = true;
+ await element.updateComplete;
+ assert.isTrue(addListenerStub.called);
+ });
+
+ test('clean up listeners when has-tooltip changed to false', async () => {
+ const removeListenerStub = sinon.stub(element, 'removeEventListener');
+ element.hasTooltip = true;
+ await element.updateComplete;
+ element.hasTooltip = false;
+ await element.updateComplete;
+ assert.isTrue(removeListenerStub.called);
+ });
+
+ test('do not display tooltips on touch devices', async () => {
+ // On touch devices, tooltips should not be shown.
+ element.isTouchDevice = true;
+ await element.updateComplete;
+
+ // fire mouse-enter
+ element._handleShowTooltip();
+ await element.updateComplete;
+ assert.isNotOk(element.tooltip);
+
+ // fire mouse-enter
+ element._handleHideTooltip();
+ await element.updateComplete;
+ assert.isNotOk(element.tooltip);
+
+ // On other devices, tooltips should be shown.
+ element.isTouchDevice = false;
+
+ // fire mouse-enter
+ element._handleShowTooltip();
+ await element.updateComplete;
+ assert.isOk(element.tooltip);
+
+ // fire mouse-enter
+ element._handleHideTooltip();
+ await element.updateComplete;
+ assert.isNotOk(element.tooltip);
});
});
diff --git a/polygerrit-ui/app/elements/shared/gr-vote-chip/gr-vote-chip.ts b/polygerrit-ui/app/elements/shared/gr-vote-chip/gr-vote-chip.ts
index 73ca99d..9013088 100644
--- a/polygerrit-ui/app/elements/shared/gr-vote-chip/gr-vote-chip.ts
+++ b/polygerrit-ui/app/elements/shared/gr-vote-chip/gr-vote-chip.ts
@@ -16,7 +16,12 @@
*/
import {LitElement, css, html} from 'lit';
import {customElement, property} from 'lit/decorators';
-import {ApprovalInfo, LabelInfo} from '../../../api/rest-api';
+import {
+ ApprovalInfo,
+ isDetailedLabelInfo,
+ isQuickLabelInfo,
+ LabelInfo,
+} from '../../../api/rest-api';
import {appContext} from '../../../services/app-context';
import {KnownExperimentId} from '../../../services/flags/flags';
import {
@@ -45,7 +50,7 @@
private readonly flagsService = appContext.flagsService;
- static get styles() {
+ static override get styles() {
return [
css`
.vote-chip.max {
@@ -83,8 +88,8 @@
display: flex;
width: var(--gr-vote-chip-width, 16px);
height: var(--gr-vote-chip-height, 16px);
+ font-size: var(--font-size-small);
justify-content: center;
- margin-right: var(--spacing-s);
padding: 1px;
border-radius: var(--border-radius);
line-height: var(--gr-vote-chip-width, 16px);
@@ -106,25 +111,54 @@
];
}
- render() {
+ override render() {
if (!this.flagsService.isEnabled(KnownExperimentId.SUBMIT_REQUIREMENTS_UI))
return;
- if (!this.vote?.value) return;
- const className = this.computeClass(this.vote.value, this.label);
+
+ const renderValue = this.renderValue();
+ if (!renderValue) return;
+
return html`<span class="container">
- <div class="vote-chip ${className} ${this.more ? 'more' : ''}">
- ${valueString(this.vote.value)}
+ <div class="vote-chip ${this.computeClass()} ${this.more ? 'more' : ''}">
+ ${renderValue}
</div>
${this.more
- ? html`<div class="chip-angle ${className}">
- ${valueString(this.vote.value)}
+ ? html`<div class="chip-angle ${this.computeClass()}">
+ ${renderValue}
</div>`
: ''}
</span>`;
}
- computeClass(vote: number, label?: LabelInfo) {
- const status = getLabelStatus(label, vote);
- return classForLabelStatus(status);
+ private renderValue() {
+ if (!this.label) {
+ return '';
+ } else if (isDetailedLabelInfo(this.label)) {
+ if (this.vote?.value) {
+ return valueString(this.vote.value);
+ }
+ } else if (isQuickLabelInfo(this.label)) {
+ if (this.label.approved) {
+ return '👍️';
+ } else if (this.label.rejected) {
+ return '👎️';
+ }
+ }
+ return '';
+ }
+
+ private computeClass() {
+ if (!this.label) {
+ return '';
+ } else if (isDetailedLabelInfo(this.label)) {
+ if (this.vote?.value) {
+ const status = getLabelStatus(this.label, this.vote.value);
+ return classForLabelStatus(status);
+ }
+ } else if (isQuickLabelInfo(this.label)) {
+ const status = getLabelStatus(this.label);
+ return classForLabelStatus(status);
+ }
+ return '';
}
}
diff --git a/polygerrit-ui/app/empty_test.sh b/polygerrit-ui/app/empty_test.sh
deleted file mode 100755
index e69de29..0000000
--- a/polygerrit-ui/app/empty_test.sh
+++ /dev/null
diff --git a/polygerrit-ui/app/mixins/gr-change-table-mixin/gr-change-table-mixin.ts b/polygerrit-ui/app/mixins/gr-change-table-mixin/gr-change-table-mixin.ts
deleted file mode 100644
index 9e4608f..0000000
--- a/polygerrit-ui/app/mixins/gr-change-table-mixin/gr-change-table-mixin.ts
+++ /dev/null
@@ -1,118 +0,0 @@
-/**
- * @license
- * Copyright (C) 2016 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 {PolymerElement} from '@polymer/polymer';
-import {Constructor} from '../../utils/common-util';
-import {property} from '@polymer/decorators';
-import {ServerInfo} from '../../types/common';
-
-/**
- * @polymer
- * @mixinFunction
- */
-export const ChangeTableMixin = <T extends Constructor<PolymerElement>>(
- superClass: T
-) => {
- /**
- * @polymer
- * @mixinClass
- */
- class Mixin extends superClass {
- @property({type: Array})
- readonly columnNames: string[] = [
- 'Subject',
- 'Status',
- 'Owner',
- 'Assignee',
- 'Reviewers',
- 'Comments',
- 'Repo',
- 'Branch',
- 'Updated',
- 'Size',
- ];
-
- isColumnHidden(columnToCheck?: string, columnsToDisplay?: string[]) {
- if (!columnsToDisplay || !columnToCheck) {
- return false;
- }
- return !columnsToDisplay.includes(columnToCheck);
- }
-
- /**
- * Is the column disabled by a server config or experiment? For example the
- * assignee feature might be disabled and thus the corresponding column is
- * also disabled.
- *
- */
- isColumnEnabled(column: string, config: ServerInfo, experiments: string[]) {
- if (!config || !config.change) return true;
- if (column === 'Assignee') return !!config.change.enable_assignee;
- if (column === 'Comments') return experiments.includes('comments-column');
- return true;
- }
-
- /**
- * @return enabled columns, see isColumnEnabled().
- */
- getEnabledColumns(
- columns: string[],
- config: ServerInfo,
- experiments: string[]
- ) {
- return columns.filter(col =>
- this.isColumnEnabled(col, config, experiments)
- );
- }
-
- /**
- * The Project column was renamed to Repo, but some users may have
- * preferences that use its old name. If that column is found, rename it
- * before use.
- *
- * @return If the column was renamed, returns a new array
- * with the corrected name. Otherwise, it returns the original param.
- */
- renameProjectToRepoColumn(columns: string[]) {
- const projectIndex = columns.indexOf('Project');
- if (projectIndex === -1) {
- return columns;
- }
- const newColumns = [...columns];
- newColumns[projectIndex] = 'Repo';
- return newColumns;
- }
- }
-
- return Mixin as T & Constructor<ChangeTableMixinInterface>;
-};
-
-export interface ChangeTableMixinInterface {
- readonly columnNames: string[];
- isColumnHidden(columnToCheck?: string, columnsToDisplay?: string[]): boolean;
- isColumnEnabled(
- column: string,
- config: ServerInfo,
- experiments: string[]
- ): boolean;
- getEnabledColumns(
- columns: string[],
- config: ServerInfo,
- experiments: string[]
- ): string[];
- renameProjectToRepoColumn(columns: string[]): string[];
-}
diff --git a/polygerrit-ui/app/mixins/gr-change-table-mixin/gr-change-table-mixin_test.js b/polygerrit-ui/app/mixins/gr-change-table-mixin/gr-change-table-mixin_test.js
deleted file mode 100644
index 8bc223f..0000000
--- a/polygerrit-ui/app/mixins/gr-change-table-mixin/gr-change-table-mixin_test.js
+++ /dev/null
@@ -1,81 +0,0 @@
-/**
- * @license
- * Copyright (C) 2016 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 {ChangeTableMixin} from './gr-change-table-mixin.js';
-import {PolymerElement} from '@polymer/polymer/polymer-element.js';
-
-// This avoids JSC_DYNAMIC_EXTENDS_WITHOUT_JSDOC closure compiler error.
-const base = ChangeTableMixin(PolymerElement);
-
-class GrChangeTableMixinTestElement extends base {
- static get is() { return 'gr-change-table-mixin-test-element'; }
-}
-
-customElements.define(GrChangeTableMixinTestElement.is,
- GrChangeTableMixinTestElement);
-
-const basicFixture = fixtureFromElement(
- 'gr-change-table-mixin-test-element');
-
-suite('gr-change-table-mixin tests', () => {
- let element;
-
- setup(() => {
- element = basicFixture.instantiate();
- });
-
- test('isColumnHidden', () => {
- const columnToCheck = 'Repo';
- let columnsToDisplay = [
- 'Subject',
- 'Status',
- 'Owner',
- 'Assignee',
- 'Repo',
- 'Branch',
- 'Updated',
- 'Size',
- ];
- assert.isFalse(element.isColumnHidden(columnToCheck, columnsToDisplay));
-
- columnsToDisplay = [
- 'Subject',
- 'Status',
- 'Owner',
- 'Assignee',
- 'Branch',
- 'Updated',
- 'Size',
- ];
- assert.isTrue(element.isColumnHidden(columnToCheck, columnsToDisplay));
- });
-
- test('renameProjectToRepoColumn maps Project to Repo', () => {
- const columns = [
- 'Subject',
- 'Status',
- 'Owner',
- ];
- assert.deepEqual(element.renameProjectToRepoColumn(columns),
- columns.slice(0));
- assert.deepEqual(
- element.renameProjectToRepoColumn(columns.concat(['Project'])),
- columns.slice(0).concat(['Repo']));
- });
-});
-
diff --git a/polygerrit-ui/app/mixins/gr-list-view-mixin/gr-list-view-mixin.ts b/polygerrit-ui/app/mixins/gr-list-view-mixin/gr-list-view-mixin.ts
deleted file mode 100644
index 70d212c..0000000
--- a/polygerrit-ui/app/mixins/gr-list-view-mixin/gr-list-view-mixin.ts
+++ /dev/null
@@ -1,75 +0,0 @@
-/**
- * @license
- * Copyright (C) 2017 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 {encodeURL, getBaseUrl} from '../../utils/url-util';
-import {PolymerElement} from '@polymer/polymer';
-import {Constructor} from '../../utils/common-util';
-
-/**
- * @polymer
- * @mixinFunction
- */
-export const ListViewMixin = <T extends Constructor<PolymerElement>>(
- superClass: T
-) => {
- /**
- * @polymer
- * @mixinClass
- */
- class Mixin extends superClass {
- computeLoadingClass(loading: boolean): string {
- return loading ? 'loading' : '';
- }
-
- computeShownItems<T>(items: T[]): T[] {
- return items.slice(0, 25);
- }
-
- getUrl(path: string, item: string) {
- return getBaseUrl() + path + encodeURL(item, true);
- }
-
- getFilterValue<T extends ListViewParams>(params: T): string {
- if (!params) {
- return '';
- }
- return params.filter || '';
- }
-
- getOffsetValue<T extends ListViewParams>(params: T): number {
- if (params?.offset) {
- return Number(params.offset);
- }
- return 0;
- }
- }
-
- return Mixin as T & Constructor<ListViewMixinInterface>;
-};
-
-export interface ListViewMixinInterface {
- computeLoadingClass(loading: boolean): string;
- computeShownItems<T>(items: T[]): T[];
- getUrl(path: string, item: string): string;
- getFilterValue<T extends ListViewParams>(params: T): string;
- getOffsetValue<T extends ListViewParams>(params: T): number;
-}
-
-export interface ListViewParams {
- filter?: string | null;
- offset?: number | string;
-}
diff --git a/polygerrit-ui/app/mixins/gr-list-view-mixin/gr-list-view-mixin_test.js b/polygerrit-ui/app/mixins/gr-list-view-mixin/gr-list-view-mixin_test.js
deleted file mode 100644
index d2b429f..0000000
--- a/polygerrit-ui/app/mixins/gr-list-view-mixin/gr-list-view-mixin_test.js
+++ /dev/null
@@ -1,80 +0,0 @@
-/**
- * @license
- * Copyright (C) 2017 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 {ListViewMixin} from './gr-list-view-mixin.js';
-import {PolymerElement} from '@polymer/polymer/polymer-element.js';
-
-const basicFixture = fixtureFromElement(
- 'gr-list-view-mixin-test-element');
-
-// This avoids JSC_DYNAMIC_EXTENDS_WITHOUT_JSDOC closure compiler error.
-const base = ListViewMixin(PolymerElement);
-
-class GrListViewMixinTestElement extends base {
- static get is() { return 'gr-list-view-mixin-test-element'; }
-}
-
-customElements.define(GrListViewMixinTestElement.is,
- GrListViewMixinTestElement);
-
-suite('gr-list-view-mixin tests', () => {
- let element;
-
- setup(() => {
- element = basicFixture.instantiate();
- });
-
- test('computeLoadingClass', () => {
- assert.equal(element.computeLoadingClass(true), 'loading');
- assert.equal(element.computeLoadingClass(false), '');
- });
-
- test('computeShownItems', () => {
- const myArr = new Array(26);
- assert.equal(element.computeShownItems(myArr).length, 25);
- });
-
- test('getUrl', () => {
- assert.equal(element.getUrl('/path/to/something/', 'item'),
- '/path/to/something/item');
- assert.equal(element.getUrl('/path/to/something/', 'item%test'),
- '/path/to/something/item%2525test');
- });
-
- test('getFilterValue', () => {
- let params;
- assert.equal(element.getFilterValue(params), '');
-
- params = {filter: null};
- assert.equal(element.getFilterValue(params), '');
-
- params = {filter: 'test'};
- assert.equal(element.getFilterValue(params), 'test');
- });
-
- test('getOffsetValue', () => {
- let params;
- assert.equal(element.getOffsetValue(params), 0);
-
- params = {offset: null};
- assert.equal(element.getOffsetValue(params), 0);
-
- params = {offset: 1};
- assert.equal(element.getOffsetValue(params), 1);
- });
-});
-
diff --git a/polygerrit-ui/app/mixins/gr-tooltip-mixin/gr-tooltip-mixin.ts b/polygerrit-ui/app/mixins/gr-tooltip-mixin/gr-tooltip-mixin.ts
deleted file mode 100644
index 3e20d1d..0000000
--- a/polygerrit-ui/app/mixins/gr-tooltip-mixin/gr-tooltip-mixin.ts
+++ /dev/null
@@ -1,227 +0,0 @@
-/**
- * @license
- * Copyright (C) 2016 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 '../../elements/shared/gr-tooltip/gr-tooltip';
-import {flush} from '@polymer/polymer/lib/legacy/polymer.dom';
-import {getRootElement} from '../../scripts/rootElement';
-import {property, observe} from '@polymer/decorators';
-import {GrTooltip} from '../../elements/shared/gr-tooltip/gr-tooltip';
-import {PolymerElement} from '@polymer/polymer';
-import {Constructor} from '../../utils/common-util';
-
-const BOTTOM_OFFSET = 7.2; // Height of the arrow in tooltip.
-
-/** The interface corresponding to TooltipMixin */
-export interface TooltipMixinInterface {
- hasTooltip: boolean;
- positionBelow: boolean;
- _isTouchDevice: boolean;
- _tooltip: GrTooltip | null;
- _titleText: string;
- _hasSetupTooltipListeners: boolean;
-}
-
-/**
- * @polymer
- * @mixinFunction
- */
-export const TooltipMixin = <T extends Constructor<PolymerElement>>(
- superClass: T
-) => {
- /**
- * @polymer
- * @mixinClass
- */
- class Mixin extends superClass {
- @property({type: Boolean})
- hasTooltip = false;
-
- @property({type: Boolean, reflectToAttribute: true})
- positionBelow = false;
-
- @property({type: Boolean})
- _isTouchDevice = 'ontouchstart' in document.documentElement;
-
- @property({type: Object})
- _tooltip: GrTooltip | null = null;
-
- @property({type: String})
- _titleText = '';
-
- @property({type: Boolean})
- _hasSetupTooltipListeners = false;
-
- // Handler for mouseenter event
- private mouseenterHandler?: (e: MouseEvent) => void;
-
- // Handler for scrolling on window
- private readonly windowScrollHandler: () => void;
-
- // Handler for showing the tooltip, will be attached to certain events
- private readonly showHandler: () => void;
-
- // Handler for hiding the tooltip, will be attached to certain events
- private readonly hideHandler: (e: Event) => void;
-
- // eslint-disable-next-line @typescript-eslint/no-unused-vars, @typescript-eslint/no-explicit-any
- constructor(..._: any[]) {
- super();
- this.windowScrollHandler = () => this._handleWindowScroll();
- this.showHandler = () => this._handleShowTooltip();
- this.hideHandler = (e: Event | undefined) => this._handleHideTooltip(e);
- }
-
- override disconnectedCallback() {
- // NOTE: if you define your own `detached` in your component
- // then this won't take affect (as its not a class yet)
- this._handleHideTooltip(undefined);
- if (this.mouseenterHandler) {
- this.removeEventListener('mouseenter', this.mouseenterHandler);
- }
- window.removeEventListener('scroll', this.windowScrollHandler);
- super.disconnectedCallback();
- }
-
- @observe('hasTooltip')
- _setupTooltipListeners() {
- if (!this.mouseenterHandler) {
- this.mouseenterHandler = this.showHandler;
- }
-
- if (!this.hasTooltip) {
- // if attribute set to false, remove the listener
- this.removeEventListener('mouseenter', this.mouseenterHandler);
- this._hasSetupTooltipListeners = false;
- return;
- }
-
- if (this._hasSetupTooltipListeners) {
- return;
- }
- this._hasSetupTooltipListeners = true;
-
- this.addEventListener('mouseenter', this.mouseenterHandler);
- }
-
- _handleShowTooltip() {
- if (this._isTouchDevice) {
- return;
- }
-
- if (
- !this.hasAttribute('title') ||
- this.getAttribute('title') === '' ||
- this._tooltip
- ) {
- return;
- }
-
- // Store the title attribute text then set it to an empty string to
- // prevent it from showing natively.
- this._titleText = this.getAttribute('title') || '';
- this.setAttribute('title', '');
-
- const tooltip = document.createElement('gr-tooltip');
- tooltip.text = this._titleText;
- tooltip.maxWidth = this.getAttribute('max-width') || '';
- tooltip.positionBelow = this.hasAttribute('position-below');
-
- // Set visibility to hidden before appending to the DOM so that
- // calculations can be made based on the element’s size.
- tooltip.style.visibility = 'hidden';
- getRootElement().appendChild(tooltip);
- this._positionTooltip(tooltip);
- tooltip.style.visibility = 'initial';
-
- this._tooltip = tooltip;
- window.addEventListener('scroll', this.windowScrollHandler);
- this.addEventListener('mouseleave', this.hideHandler);
- this.addEventListener('click', this.hideHandler);
- tooltip.addEventListener('mouseleave', this.hideHandler);
- }
-
- _handleHideTooltip(e: Event | undefined) {
- if (this._isTouchDevice) {
- return;
- }
- if (!this.hasAttribute('title') || !this._titleText) {
- return;
- }
- // Do not hide if mouse left this or this._tooltip and came to this or
- // this._tooltip
- if (
- (e as MouseEvent)?.relatedTarget === this._tooltip ||
- (e as MouseEvent)?.relatedTarget === this
- ) {
- return;
- }
-
- window.removeEventListener('scroll', this.windowScrollHandler);
- this.removeEventListener('mouseleave', this.hideHandler);
- this.removeEventListener('click', this.hideHandler);
- this.setAttribute('title', this._titleText);
- this._tooltip?.removeEventListener('mouseleave', this.hideHandler);
-
- if (this._tooltip?.parentNode) {
- this._tooltip.parentNode.removeChild(this._tooltip);
- }
- this._tooltip = null;
- }
-
- _handleWindowScroll() {
- if (!this._tooltip) {
- return;
- }
-
- this._positionTooltip(this._tooltip);
- }
-
- _positionTooltip(tooltip: GrTooltip) {
- // This flush is needed for tooltips to be positioned correctly in Firefox
- // and Safari.
- flush();
- const rect = this.getBoundingClientRect();
- const boxRect = tooltip.getBoundingClientRect();
- if (!tooltip.parentElement) {
- return;
- }
- const parentRect = tooltip.parentElement.getBoundingClientRect();
- const top = rect.top - parentRect.top;
- const left =
- rect.left - parentRect.left + (rect.width - boxRect.width) / 2;
- const right = parentRect.width - left - boxRect.width;
- if (left < 0) {
- tooltip.updateStyles({
- '--gr-tooltip-arrow-center-offset': `${left}px`,
- });
- } else if (right < 0) {
- tooltip.updateStyles({
- '--gr-tooltip-arrow-center-offset': `${-0.5 * right}px`,
- });
- }
- tooltip.style.left = `${Math.max(0, left)}px`;
-
- if (!this.positionBelow) {
- tooltip.style.top = `${Math.max(0, top)}px`;
- tooltip.style.transform = `translateY(calc(-100% - ${BOTTOM_OFFSET}px))`;
- } else {
- tooltip.style.top = `${top + rect.height + BOTTOM_OFFSET}px`;
- }
- }
- }
-
- return Mixin as T & Constructor<TooltipMixinInterface>;
-};
diff --git a/polygerrit-ui/app/mixins/gr-tooltip-mixin/gr-tooltip-mixin_test.js b/polygerrit-ui/app/mixins/gr-tooltip-mixin/gr-tooltip-mixin_test.js
deleted file mode 100644
index 69e6e86..0000000
--- a/polygerrit-ui/app/mixins/gr-tooltip-mixin/gr-tooltip-mixin_test.js
+++ /dev/null
@@ -1,140 +0,0 @@
-/**
- * @license
- * Copyright (C) 2017 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 {PolymerElement} from '@polymer/polymer/polymer-element.js';
-import {TooltipMixin} from './gr-tooltip-mixin.js';
-
-const basicFixture = fixtureFromElement('gr-tooltip-mixin-element');
-
-// This avoids JSC_DYNAMIC_EXTENDS_WITHOUT_JSDOC closure compiler error.
-const base = TooltipMixin(PolymerElement);
-
-class GrTooltipMixinTestElement extends base {
- static get is() {
- return 'gr-tooltip-mixin-element';
- }
-}
-
-customElements.define(GrTooltipMixinTestElement.is,
- GrTooltipMixinTestElement);
-
-suite('gr-tooltip-mixin tests', () => {
- let element;
-
- function makeTooltip(tooltipRect, parentRect) {
- return {
- getBoundingClientRect() { return tooltipRect; },
- updateStyles: sinon.stub(),
- style: {left: 0, top: 0},
- parentElement: {
- getBoundingClientRect() { return parentRect; },
- },
- };
- }
-
- setup(() => {
- element = basicFixture.instantiate();
- });
-
- test('normal position', () => {
- sinon.stub(element, 'getBoundingClientRect').callsFake(() => {
- return {top: 100, left: 100, width: 200};
- });
- const tooltip = makeTooltip(
- {height: 30, width: 50},
- {top: 0, left: 0, width: 1000});
-
- element._positionTooltip(tooltip);
- assert.isFalse(tooltip.updateStyles.called);
- assert.equal(tooltip.style.left, '175px');
- assert.equal(tooltip.style.top, '100px');
- });
-
- test('left side position', () => {
- sinon.stub(element, 'getBoundingClientRect').callsFake(() => {
- return {top: 100, left: 10, width: 50};
- });
- const tooltip = makeTooltip(
- {height: 30, width: 120},
- {top: 0, left: 0, width: 1000});
-
- element._positionTooltip(tooltip);
- assert.isTrue(tooltip.updateStyles.called);
- const offset = tooltip.updateStyles
- .lastCall.args[0]['--gr-tooltip-arrow-center-offset'];
- assert.isBelow(parseFloat(offset.replace(/px$/, '')), 0);
- assert.equal(tooltip.style.left, '0px');
- assert.equal(tooltip.style.top, '100px');
- });
-
- test('right side position', () => {
- sinon.stub(element, 'getBoundingClientRect').callsFake(() => {
- return {top: 100, left: 950, width: 50};
- });
- const tooltip = makeTooltip(
- {height: 30, width: 120},
- {top: 0, left: 0, width: 1000});
-
- element._positionTooltip(tooltip);
- assert.isTrue(tooltip.updateStyles.called);
- const offset = tooltip.updateStyles
- .lastCall.args[0]['--gr-tooltip-arrow-center-offset'];
- assert.isAbove(parseFloat(offset.replace(/px$/, '')), 0);
- assert.equal(tooltip.style.left, '915px');
- assert.equal(tooltip.style.top, '100px');
- });
-
- test('position to bottom', () => {
- sinon.stub(element, 'getBoundingClientRect').callsFake(() => {
- return {top: 100, left: 950, width: 50, height: 50};
- });
- const tooltip = makeTooltip(
- {height: 30, width: 120},
- {top: 0, left: 0, width: 1000});
-
- element.positionBelow = true;
- element._positionTooltip(tooltip);
- assert.isTrue(tooltip.updateStyles.called);
- const offset = tooltip.updateStyles
- .lastCall.args[0]['--gr-tooltip-arrow-center-offset'];
- assert.isAbove(parseFloat(offset.replace(/px$/, '')), 0);
- assert.equal(tooltip.style.left, '915px');
- assert.equal(tooltip.style.top, '157.2px');
- });
-
- test('hides tooltip when detached', () => {
- sinon.stub(element, '_handleHideTooltip');
- element.remove();
- flush();
- assert.isTrue(element._handleHideTooltip.called);
- });
-
- test('sets up listeners when has-tooltip is changed', () => {
- const addListenerStub = sinon.stub(element, 'addEventListener');
- element.hasTooltip = true;
- assert.isTrue(addListenerStub.called);
- });
-
- test('clean up listeners when has-tooltip changed to false', () => {
- const removeListenerStub = sinon.stub(element, 'removeEventListener');
- element.hasTooltip = true;
- element.hasTooltip = false;
- assert.isTrue(removeListenerStub.called);
- });
-});
-
diff --git a/polygerrit-ui/app/elements/shared/gr-hovercard/gr-hovercard-behavior.ts b/polygerrit-ui/app/mixins/hovercard-mixin/hovercard-mixin.ts
similarity index 81%
rename from polygerrit-ui/app/elements/shared/gr-hovercard/gr-hovercard-behavior.ts
rename to polygerrit-ui/app/mixins/hovercard-mixin/hovercard-mixin.ts
index 2bb9de5..793e5d6 100644
--- a/polygerrit-ui/app/elements/shared/gr-hovercard/gr-hovercard-behavior.ts
+++ b/polygerrit-ui/app/mixins/hovercard-mixin/hovercard-mixin.ts
@@ -14,18 +14,15 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-import '../../../styles/shared-styles';
-import {flush} from '@polymer/polymer/lib/legacy/polymer.dom';
-import {getRootElement} from '../../../scripts/rootElement';
-import {Constructor} from '../../../utils/common-util';
-import {PolymerElement} from '@polymer/polymer/polymer-element';
-import {property, observe} from '@polymer/decorators';
-import {
- pushScrollLock,
- removeScrollLock,
-} from '@polymer/iron-overlay-behavior/iron-scroll-manager';
-import {ShowAlertEventDetail} from '../../../types/events';
-import {debounce, DelayedTask} from '../../../utils/async-util';
+import {getRootElement} from '../../scripts/rootElement';
+import {Constructor} from '../../utils/common-util';
+import {LitElement, PropertyValues} from 'lit';
+import {property, query} from 'lit/decorators';
+import {ShowAlertEventDetail} from '../../types/events';
+import {debounce, DelayedTask} from '../../utils/async-util';
+import {hovercardStyles} from '../../styles/gr-hovercard-styles';
+import {sharedStyles} from '../../styles/shared-styles';
+
interface ReloadEventDetail {
clearPatchset?: boolean;
}
@@ -68,27 +65,30 @@
const HIDE_DELAY_MS = 500;
/**
- * The mixin for gr-hovercard-behavior.
+ * The mixin for hovercard behavior.
*
* @example
*
* class YourComponent extends hovercardBehaviorMixin(
- * PolymerElement
+ * LitElement)
*
* @see gr-hovercard.ts
*
* // following annotations are required for polylint
- * @polymer
+ * @lit
* @mixinFunction
*/
-export const HovercardBehaviorMixin = <T extends Constructor<PolymerElement>>(
+export const HovercardMixin = <T extends Constructor<LitElement>>(
superClass: T
) => {
/**
- * @polymer
+ * @lit
* @mixinClass
*/
class Mixin extends superClass {
+ @query('#container')
+ topElement?: HTMLElement;
+
@property({type: Object})
_target: HTMLElement | null = null;
@@ -117,50 +117,56 @@
@property({type: Object})
container: HTMLElement | null = null;
- private hideTask?: DelayedTask;
+ // Private but used in tests.
+ hideTask?: DelayedTask;
- private showTask?: DelayedTask;
+ showTask?: DelayedTask;
- private isScheduledToShow?: boolean;
+ isScheduledToShow?: boolean;
- private isScheduledToHide?: boolean;
+ isScheduledToHide?: boolean;
- override connectedCallback() {
- super.connectedCallback();
- if (!this._target) {
- this._target = this.target;
- }
- this._target.addEventListener('mouseenter', this.debounceShow);
- this._target.addEventListener('focus', this.debounceShow);
- this._target.addEventListener('mouseleave', this.debounceHide);
- this._target.addEventListener('blur', this.debounceHide);
+ static get styles() {
+ return [sharedStyles, hovercardStyles];
+ }
- // when click, dismiss immediately
- this._target.addEventListener('click', this.hide);
-
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ constructor(...args: any[]) {
+ super(...args);
// show the hovercard if mouse moves to hovercard
// this will cancel pending hide as well
this.addEventListener('mouseenter', this.show);
- this.addEventListener('mouseenter', this.lock);
// when leave hovercard, hide it immediately
this.addEventListener('mouseleave', this.hide);
- this.addEventListener('mouseleave', this.unlock);
+ }
+
+ override connectedCallback() {
+ super.connectedCallback();
+ // We have to cache the target because when we this.container.appendChild
+ // in show we can not pick the container as target when we reconnect.
+ if (!this._target) {
+ this._target = this.target;
+ this.addTargetEventListeners();
+ }
+
+ this.container = getHovercardContainer({createIfNotExists: true});
}
override disconnectedCallback() {
this.cancelShowTask();
this.cancelHideTask();
- this.unlock();
super.disconnectedCallback();
}
- override ready() {
- super.ready();
- // First, check to see if the container has already been created.
- this.container = getHovercardContainer({createIfNotExists: true});
+ private addTargetEventListeners() {
+ this._target?.addEventListener('mouseenter', this.debounceShow);
+ this._target?.addEventListener('focus', this.debounceShow);
+ this._target?.addEventListener('mouseleave', this.debounceHide);
+ this._target?.addEventListener('blur', this.debounceHide);
+ this._target?.addEventListener('click', this.hide);
}
- removeListeners() {
+ private removeTargetEventListeners() {
this._target?.removeEventListener('mouseenter', this.debounceShow);
this._target?.removeEventListener('focus', this.debounceShow);
this._target?.removeEventListener('mouseleave', this.debounceHide);
@@ -168,6 +174,19 @@
this._target?.removeEventListener('click', this.hide);
}
+ /**
+ * Responds to a change in the `for` value and gets the updated `target`
+ * element for the hovercard.
+ */
+ override updated(changedProperties: PropertyValues) {
+ super.updated(changedProperties);
+ if (changedProperties.has('for')) {
+ this.removeTargetEventListeners();
+ this._target = this.target;
+ this.addTargetEventListeners();
+ }
+ }
+
readonly debounceHide = () => {
this.cancelShowTask();
if (!this._isShowing || this.isScheduledToHide) return;
@@ -185,10 +204,10 @@
};
cancelHideTask() {
- if (this.hideTask) {
- this.hideTask.cancel();
- this.isScheduledToHide = false;
- }
+ if (!this.hideTask) return;
+ this.hideTask.cancel();
+ this.isScheduledToHide = false;
+ this.hideTask = undefined;
}
/**
@@ -241,13 +260,6 @@
}
/**
- * unlock scroll, this will resume the scroll outside of the hovercard.
- */
- readonly unlock = () => {
- removeScrollLock(this);
- };
-
- /**
* Hides/closes the hovercard. This occurs when the user triggers the
* `mouseleave` event on the hovercard's `target` element (as long as the
* user is not hovering over the hovercard).
@@ -280,7 +292,7 @@
// Reset and remove the hovercard from the DOM
this.style.cssText = '';
- this.$['container'].setAttribute('tabindex', '-1');
+ this.topElement?.setAttribute('tabindex', '-1');
// Remove the hovercard from the container, given that it is still a child
// of the container.
@@ -315,24 +327,17 @@
}
cancelShowTask() {
- if (this.showTask) {
- this.showTask.cancel();
- this.isScheduledToShow = false;
- }
+ if (!this.showTask) return;
+ this.showTask.cancel();
+ this.isScheduledToShow = false;
+ this.showTask = undefined;
}
/**
- * Lock background scroll but enable scroll inside of current hovercard.
- */
- readonly lock = () => {
- pushScrollLock(this);
- };
-
- /**
* Shows/opens the hovercard. This occurs when the user triggers the
* `mousenter` event on the hovercard's `target` element.
*/
- readonly show = () => {
+ readonly show = async () => {
this.cancelHideTask();
this.cancelShowTask();
if (this._isShowing || !this.container) {
@@ -352,7 +357,9 @@
// Make sure that the hovercard actually rendered and all dom-if
// statements processed, so that we can measure the (invisible)
// hovercard properly in updatePosition().
- flush();
+ await new Promise<void>(r => {
+ setTimeout(r, 0);
+ });
this.updatePosition();
this.classList.remove(HIDE_CLASS);
};
@@ -455,31 +462,27 @@
this.style.left = `${hovercardLeft}px`;
this.style.top = `${hovercardTop}px`;
}
-
- /**
- * Responds to a change in the `for` value and gets the updated `target`
- * element for the hovercard.
- */
- @observe('for')
- _forChanged() {
- this._target = this.target;
- }
}
- return Mixin as T & Constructor<GrHovercardBehaviorInterface>;
+ return Mixin as T & Constructor<HovercardMixinInterface>;
};
-export interface GrHovercardBehaviorInterface {
- ready(): void;
- removeListeners(): void;
- debounceHide(): void;
- cancelHideTask(): void;
+export interface HovercardMixinInterface {
+ for?: string;
+ offset: number;
+ _target: HTMLElement | null;
+ _isShowing: boolean;
dispatchEventThroughTarget(eventName: string, detail?: unknown): void;
- hide(e?: MouseEvent): void;
- debounceShow(): void;
- debounceShowBy(delayMs: number): void;
- cancelShowTask(): void;
show(): void;
+
+ // Used for tests
+ hide(e: MouseEvent): void;
+ container: HTMLElement | null;
+ hideTask?: DelayedTask;
+ showTask?: DelayedTask;
+ position: string;
+ debounceShowBy(delayMs: number): void;
updatePosition(): void;
- updatePositionTo(position: string): void;
+ isScheduledToShow?: boolean;
+ isScheduledToHide?: boolean;
}
diff --git a/polygerrit-ui/app/mixins/hovercard-mixin/hovercard-mixin_test.ts b/polygerrit-ui/app/mixins/hovercard-mixin/hovercard-mixin_test.ts
new file mode 100644
index 0000000..bd12789
--- /dev/null
+++ b/polygerrit-ui/app/mixins/hovercard-mixin/hovercard-mixin_test.ts
@@ -0,0 +1,177 @@
+/**
+ * @license
+ * Copyright (C) 2018 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 {HovercardMixin} from './hovercard-mixin.js';
+import {html, LitElement} from 'lit';
+import {customElement} from 'lit/decorators';
+import {MockPromise, mockPromise} from '../../test/test-utils.js';
+
+const base = HovercardMixin(LitElement);
+
+declare global {
+ interface HTMLElementTagNameMap {
+ 'hovercard-mixin-test': HovercardMixinTest;
+ }
+}
+
+@customElement('hovercard-mixin-test')
+class HovercardMixinTest extends base {
+ constructor() {
+ super();
+ this.for = 'foo';
+ }
+
+ override render() {
+ return html`<div id="container"><slot></slot></div>`;
+ }
+}
+
+const basicFixture = fixtureFromElement('hovercard-mixin-test');
+
+suite('gr-hovercard tests', () => {
+ let element: HovercardMixinTest;
+
+ let button: HTMLElement;
+ let testPromise: MockPromise;
+
+ setup(() => {
+ testPromise = mockPromise();
+ button = document.createElement('button');
+ button.innerHTML = 'Hello';
+ button.setAttribute('id', 'foo');
+ document.body.appendChild(button);
+
+ element = basicFixture.instantiate();
+ });
+
+ teardown(() => {
+ element.hide(new MouseEvent('click'));
+ button?.remove();
+ });
+
+ test('updatePosition', async () => {
+ // Test that the correct style properties have at least been set.
+ element.position = 'bottom';
+ element.updatePosition();
+ await element.updateComplete;
+ assert.typeOf(element.style.getPropertyValue('left'), 'string');
+ assert.typeOf(element.style.getPropertyValue('top'), 'string');
+ assert.typeOf(element.style.getPropertyValue('paddingTop'), 'string');
+ assert.typeOf(element.style.getPropertyValue('marginTop'), 'string');
+
+ const parentRect = document.documentElement.getBoundingClientRect();
+ const targetRect = element!._target!.getBoundingClientRect();
+ const thisRect = element.getBoundingClientRect();
+
+ const targetLeft = targetRect.left - parentRect.left;
+ const targetTop = targetRect.top - parentRect.top;
+
+ const pixelCompare = (pixel: string) =>
+ Math.round(parseInt(pixel.substring(0, pixel.length - 1), 10));
+
+ assert.equal(
+ pixelCompare(element.style.left),
+ pixelCompare(`${targetLeft + (targetRect.width - thisRect.width) / 2}px`)
+ );
+ assert.equal(
+ pixelCompare(element.style.top),
+ pixelCompare(`${targetTop + targetRect.height + element.offset}px`)
+ );
+ });
+
+ test('hide', () => {
+ element.hide(new MouseEvent('click'));
+ const style = getComputedStyle(element);
+ assert.isFalse(element._isShowing);
+ assert.isFalse(element.classList.contains('hovered'));
+ assert.equal(style.display, 'none');
+ assert.notEqual(element.container, element.parentNode);
+ });
+
+ test('show', async () => {
+ await element.show();
+ await element.updateComplete;
+ const style = getComputedStyle(element);
+ assert.isTrue(element._isShowing);
+ assert.isTrue(element.classList.contains('hovered'));
+ assert.equal(style.opacity, '1');
+ assert.equal(style.visibility, 'visible');
+ });
+
+ test('debounceShow does not show immediately', async () => {
+ element.debounceShowBy(100);
+ setTimeout(() => testPromise.resolve(), 0);
+ await testPromise;
+ assert.isFalse(element._isShowing);
+ });
+
+ test('debounceShow shows after delay', async () => {
+ element.debounceShowBy(1);
+ setTimeout(() => testPromise.resolve(), 10);
+ await testPromise;
+ assert.isTrue(element._isShowing);
+ });
+
+ test('card is scheduled to show on enter and hides on leave', async () => {
+ const button = document.querySelector('button');
+ const enterPromise = mockPromise();
+ button!.addEventListener('mouseenter', () => enterPromise.resolve());
+ const leavePromise = mockPromise();
+ button!.addEventListener('mouseleave', () => leavePromise.resolve());
+
+ assert.isFalse(element._isShowing);
+ button!.dispatchEvent(new CustomEvent('mouseenter'));
+
+ await enterPromise;
+ await flush();
+ assert.isTrue(element.isScheduledToShow);
+ element!.showTask!.flush();
+ assert.isTrue(element._isShowing);
+ assert.isFalse(element.isScheduledToShow);
+
+ button!.dispatchEvent(new CustomEvent('mouseleave'));
+
+ await leavePromise;
+ assert.isTrue(element.isScheduledToHide);
+ assert.isTrue(element._isShowing);
+ element!.hideTask!.flush();
+ assert.isFalse(element.isScheduledToShow);
+ assert.isFalse(element._isShowing);
+ });
+
+ test('card should disappear on click', async () => {
+ const button = document.querySelector('button');
+ const enterPromise = mockPromise();
+ const clickPromise = mockPromise();
+ button!.addEventListener('mouseenter', () => enterPromise.resolve());
+ button!.addEventListener('click', () => clickPromise.resolve());
+
+ assert.isFalse(element._isShowing);
+
+ button!.dispatchEvent(new CustomEvent('mouseenter'));
+
+ await enterPromise;
+ await flush();
+ assert.isTrue(element.isScheduledToShow);
+ button!.click();
+
+ await clickPromise;
+ assert.isFalse(element.isScheduledToShow);
+ assert.isFalse(element._isShowing);
+ });
+});
diff --git a/polygerrit-ui/app/mixins/keyboard-shortcut-mixin/keyboard-shortcut-mixin.ts b/polygerrit-ui/app/mixins/keyboard-shortcut-mixin/keyboard-shortcut-mixin.ts
index 5c4c9ce..3d1e120 100644
--- a/polygerrit-ui/app/mixins/keyboard-shortcut-mixin/keyboard-shortcut-mixin.ts
+++ b/polygerrit-ui/app/mixins/keyboard-shortcut-mixin/keyboard-shortcut-mixin.ts
@@ -14,104 +14,31 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-/*
-
-How to Add a Keyboard Shortcut
-==============================
-
-A keyboard shortcut is composed of the following parts:
-
- 1. A semantic identifier (e.g. OPEN_CHANGE, NEXT_PAGE)
- 2. Documentation for the keyboard shortcut help dialog
- 3. A binding between key combos and the semantic identifier
- 4. A binding between the semantic identifier and a listener
-
-Parts (1) and (2) for all shortcuts are defined in this file. The semantic
-identifier is declared in the Shortcut enum near the head of this script:
-
- const Shortcut = {
- // ...
- TOGGLE_LEFT_PANE: 'TOGGLE_LEFT_PANE',
- // ...
- };
-
-Immediately following the Shortcut enum definition, there is a _describe
-function defined which is then invoked many times to populate the help dialog.
-Add a new invocation here to document the shortcut:
-
- _describe(Shortcut.TOGGLE_LEFT_PANE, ShortcutSection.DIFFS,
- 'Hide/show left diff');
-
-When an attached view binds one or more key combos to this shortcut, the help
-dialog will display this text in the given section (in this case, "Diffs"). See
-the ShortcutSection enum immediately below for the list of supported sections.
-
-Part (3), the actual key bindings, are declared by gr-app. In the future, this
-system may be expanded to allow key binding customizations by plugins or user
-preferences. Key bindings are defined in the following forms:
-
- // Ordinary shortcut with a single binding.
- this.bindShortcut(
- Shortcut.TOGGLE_LEFT_PANE, 'shift+a');
-
- // Ordinary shortcut with multiple bindings.
- this.bindShortcut(
- Shortcut.CURSOR_NEXT_FILE, 'j', 'down');
-
- // A "go-key" keyboard shortcut, which is combined with a previously and
- // continuously pressed "go" key (the go-key is hard-coded as 'g').
- this.bindShortcut(
- Shortcut.GO_TO_OPENED_CHANGES, SPECIAL_SHORTCUT.GO_KEY, 'o');
-
- // A "doc-only" keyboard shortcut. This declares the key-binding for help
- // dialog purposes, but doesn't actually implement the binding. It is up
- // to some element to implement this binding using iron-a11y-keys-behavior's
- // keyBindings property.
- this.bindShortcut(
- Shortcut.EXPAND_ALL_COMMENT_THREADS, SPECIAL_SHORTCUT.DOC_ONLY, 'e');
-
-Part (4), the listener definitions, are declared by the view or element that
-implements the shortcut behavior. This is done by implementing a method named
-keyboardShortcuts() in an element that mixes in this behavior, returning an
-object that maps semantic identifiers (as property names) to listener method
-names, like this:
-
- keyboardShortcuts() {
- return {
- [Shortcut.TOGGLE_LEFT_PANE]: '_handleToggleLeftPane',
- };
- },
-
-You can implement key bindings in an element that is hosted by a view IF that
-element is always attached exactly once under that view (e.g. the search bar in
-gr-app). When that is not the case, you will have to define a doc-only binding
-in gr-app, declare the shortcut in the view that hosts the element, and use
-iron-a11y-keys-behavior's keyBindings attribute to implement the binding in the
-element. An example of this is in comment threads. A diff view supports actions
-on comment threads, but there may be zero or many comment threads attached at
-any given point. So the shortcut is declared as doc-only by the diff view and
-by gr-app, and actually implemented by gr-comment-thread.
-
-NOTE: doc-only shortcuts will not be customizable in the same way that other
-shortcuts are.
-*/
-
import {IronA11yKeysBehavior} from '@polymer/iron-a11y-keys-behavior/iron-a11y-keys-behavior';
-import {dom, EventApi} from '@polymer/polymer/lib/legacy/polymer.dom';
import {mixinBehaviors} from '@polymer/polymer/lib/legacy/class';
import {property} from '@polymer/decorators';
import {PolymerElement} from '@polymer/polymer';
import {check, Constructor} from '../../utils/common-util';
-import {getKeyboardEvent, isModifierPressed} from '../../utils/dom-util';
-import {CustomKeyboardEvent} from '../../types/events';
+import {isModifierPressed} from '../../utils/dom-util';
+import {IronKeyboardEvent} from '../../types/events';
import {appContext} from '../../services/app-context';
+import {
+ Shortcut,
+ ShortcutSection,
+ SPECIAL_SHORTCUT,
+} from '../../services/shortcuts/shortcuts-config';
+import {
+ ShortcutListener,
+ SectionView,
+} from '../../services/shortcuts/shortcuts-service';
-/** Enum for all special shortcuts */
-export enum SPECIAL_SHORTCUT {
- DOC_ONLY = 'DOC_ONLY',
- GO_KEY = 'GO_KEY',
- V_KEY = 'V_KEY',
-}
+export {
+ Shortcut,
+ ShortcutSection,
+ SPECIAL_SHORTCUT,
+ ShortcutListener,
+ SectionView,
+};
// The maximum age of a keydown event to be used in a jump navigation. This
// is only for cases when the keyup event is lost.
@@ -119,625 +46,6 @@
const V_KEY_TIMEOUT_MS = 1000;
-/**
- * Enum for all shortcut sections, where that shortcut should be applied to.
- */
-export enum ShortcutSection {
- ACTIONS = 'Actions',
- DIFFS = 'Diffs',
- EVERYWHERE = 'Global Shortcuts',
- FILE_LIST = 'File list',
- NAVIGATION = 'Navigation',
- REPLY_DIALOG = 'Reply dialog',
-}
-
-/**
- * Enum for all possible shortcut names.
- */
-export enum Shortcut {
- OPEN_SHORTCUT_HELP_DIALOG = 'OPEN_SHORTCUT_HELP_DIALOG',
- GO_TO_USER_DASHBOARD = 'GO_TO_USER_DASHBOARD',
- GO_TO_OPENED_CHANGES = 'GO_TO_OPENED_CHANGES',
- GO_TO_MERGED_CHANGES = 'GO_TO_MERGED_CHANGES',
- GO_TO_ABANDONED_CHANGES = 'GO_TO_ABANDONED_CHANGES',
- GO_TO_WATCHED_CHANGES = 'GO_TO_WATCHED_CHANGES',
-
- CURSOR_NEXT_CHANGE = 'CURSOR_NEXT_CHANGE',
- CURSOR_PREV_CHANGE = 'CURSOR_PREV_CHANGE',
- OPEN_CHANGE = 'OPEN_CHANGE',
- NEXT_PAGE = 'NEXT_PAGE',
- PREV_PAGE = 'PREV_PAGE',
- TOGGLE_CHANGE_REVIEWED = 'TOGGLE_CHANGE_REVIEWED',
- TOGGLE_CHANGE_STAR = 'TOGGLE_CHANGE_STAR',
- REFRESH_CHANGE_LIST = 'REFRESH_CHANGE_LIST',
- OPEN_SUBMIT_DIALOG = 'OPEN_SUBMIT_DIALOG',
-
- OPEN_REPLY_DIALOG = 'OPEN_REPLY_DIALOG',
- OPEN_DOWNLOAD_DIALOG = 'OPEN_DOWNLOAD_DIALOG',
- EXPAND_ALL_MESSAGES = 'EXPAND_ALL_MESSAGES',
- COLLAPSE_ALL_MESSAGES = 'COLLAPSE_ALL_MESSAGES',
- UP_TO_DASHBOARD = 'UP_TO_DASHBOARD',
- UP_TO_CHANGE = 'UP_TO_CHANGE',
- TOGGLE_DIFF_MODE = 'TOGGLE_DIFF_MODE',
- REFRESH_CHANGE = 'REFRESH_CHANGE',
- EDIT_TOPIC = 'EDIT_TOPIC',
- DIFF_AGAINST_BASE = 'DIFF_AGAINST_BASE',
- DIFF_AGAINST_LATEST = 'DIFF_AGAINST_LATEST',
- DIFF_BASE_AGAINST_LEFT = 'DIFF_BASE_AGAINST_LEFT',
- DIFF_RIGHT_AGAINST_LATEST = 'DIFF_RIGHT_AGAINST_LATEST',
- DIFF_BASE_AGAINST_LATEST = 'DIFF_BASE_AGAINST_LATEST',
-
- NEXT_LINE = 'NEXT_LINE',
- PREV_LINE = 'PREV_LINE',
- VISIBLE_LINE = 'VISIBLE_LINE',
- NEXT_CHUNK = 'NEXT_CHUNK',
- PREV_CHUNK = 'PREV_CHUNK',
- TOGGLE_ALL_DIFF_CONTEXT = 'TOGGLE_ALL_DIFF_CONTEXT',
- NEXT_COMMENT_THREAD = 'NEXT_COMMENT_THREAD',
- PREV_COMMENT_THREAD = 'PREV_COMMENT_THREAD',
- EXPAND_ALL_COMMENT_THREADS = 'EXPAND_ALL_COMMENT_THREADS',
- COLLAPSE_ALL_COMMENT_THREADS = 'COLLAPSE_ALL_COMMENT_THREADS',
- LEFT_PANE = 'LEFT_PANE',
- RIGHT_PANE = 'RIGHT_PANE',
- TOGGLE_LEFT_PANE = 'TOGGLE_LEFT_PANE',
- NEW_COMMENT = 'NEW_COMMENT',
- SAVE_COMMENT = 'SAVE_COMMENT',
- OPEN_DIFF_PREFS = 'OPEN_DIFF_PREFS',
- TOGGLE_DIFF_REVIEWED = 'TOGGLE_DIFF_REVIEWED',
-
- NEXT_FILE = 'NEXT_FILE',
- PREV_FILE = 'PREV_FILE',
- NEXT_FILE_WITH_COMMENTS = 'NEXT_FILE_WITH_COMMENTS',
- PREV_FILE_WITH_COMMENTS = 'PREV_FILE_WITH_COMMENTS',
- NEXT_UNREVIEWED_FILE = 'NEXT_UNREVIEWED_FILE',
- CURSOR_NEXT_FILE = 'CURSOR_NEXT_FILE',
- CURSOR_PREV_FILE = 'CURSOR_PREV_FILE',
- OPEN_FILE = 'OPEN_FILE',
- TOGGLE_FILE_REVIEWED = 'TOGGLE_FILE_REVIEWED',
- TOGGLE_ALL_INLINE_DIFFS = 'TOGGLE_ALL_INLINE_DIFFS',
- TOGGLE_INLINE_DIFF = 'TOGGLE_INLINE_DIFF',
- TOGGLE_HIDE_ALL_COMMENT_THREADS = 'TOGGLE_HIDE_ALL_COMMENT_THREADS',
- OPEN_FILE_LIST = 'OPEN_FILE_LIST',
-
- OPEN_FIRST_FILE = 'OPEN_FIRST_FILE',
- OPEN_LAST_FILE = 'OPEN_LAST_FILE',
-
- SEARCH = 'SEARCH',
- SEND_REPLY = 'SEND_REPLY',
- EMOJI_DROPDOWN = 'EMOJI_DROPDOWN',
- TOGGLE_BLAME = 'TOGGLE_BLAME',
-}
-
-export type SectionView = Array<{binding: string[][]; text: string}>;
-
-/**
- * The interface for listener for shortcut events.
- */
-export type ShortcutListener = (
- viewMap?: Map<ShortcutSection, SectionView>
-) => void;
-
-interface ShortcutHelpItem {
- shortcut: Shortcut;
- text: string;
-}
-
-// TODO(TS): rename to something more meaningful
-const _help = new Map<ShortcutSection, ShortcutHelpItem[]>();
-
-function _describe(shortcut: Shortcut, section: ShortcutSection, text: string) {
- if (!_help.has(section)) {
- _help.set(section, []);
- }
- const shortcuts = _help.get(section);
- if (shortcuts) {
- shortcuts.push({shortcut, text});
- }
-}
-
-_describe(Shortcut.SEARCH, ShortcutSection.EVERYWHERE, 'Search');
-_describe(
- Shortcut.OPEN_SHORTCUT_HELP_DIALOG,
- ShortcutSection.EVERYWHERE,
- 'Show this dialog'
-);
-_describe(
- Shortcut.GO_TO_USER_DASHBOARD,
- ShortcutSection.EVERYWHERE,
- 'Go to User Dashboard'
-);
-_describe(
- Shortcut.GO_TO_OPENED_CHANGES,
- ShortcutSection.EVERYWHERE,
- 'Go to Opened Changes'
-);
-_describe(
- Shortcut.GO_TO_MERGED_CHANGES,
- ShortcutSection.EVERYWHERE,
- 'Go to Merged Changes'
-);
-_describe(
- Shortcut.GO_TO_ABANDONED_CHANGES,
- ShortcutSection.EVERYWHERE,
- 'Go to Abandoned Changes'
-);
-_describe(
- Shortcut.GO_TO_WATCHED_CHANGES,
- ShortcutSection.EVERYWHERE,
- 'Go to Watched Changes'
-);
-
-_describe(
- Shortcut.CURSOR_NEXT_CHANGE,
- ShortcutSection.ACTIONS,
- 'Select next change'
-);
-_describe(
- Shortcut.CURSOR_PREV_CHANGE,
- ShortcutSection.ACTIONS,
- 'Select previous change'
-);
-_describe(
- Shortcut.OPEN_CHANGE,
- ShortcutSection.ACTIONS,
- 'Show selected change'
-);
-_describe(Shortcut.NEXT_PAGE, ShortcutSection.ACTIONS, 'Go to next page');
-_describe(Shortcut.PREV_PAGE, ShortcutSection.ACTIONS, 'Go to previous page');
-_describe(
- Shortcut.OPEN_REPLY_DIALOG,
- ShortcutSection.ACTIONS,
- 'Open reply dialog to publish comments and add reviewers'
-);
-_describe(
- Shortcut.OPEN_DOWNLOAD_DIALOG,
- ShortcutSection.ACTIONS,
- 'Open download overlay'
-);
-_describe(
- Shortcut.EXPAND_ALL_MESSAGES,
- ShortcutSection.ACTIONS,
- 'Expand all messages'
-);
-_describe(
- Shortcut.COLLAPSE_ALL_MESSAGES,
- ShortcutSection.ACTIONS,
- 'Collapse all messages'
-);
-_describe(
- Shortcut.REFRESH_CHANGE,
- ShortcutSection.ACTIONS,
- 'Reload the change at the latest patch'
-);
-_describe(
- Shortcut.TOGGLE_CHANGE_REVIEWED,
- ShortcutSection.ACTIONS,
- 'Mark/unmark change as reviewed'
-);
-_describe(
- Shortcut.TOGGLE_FILE_REVIEWED,
- ShortcutSection.ACTIONS,
- 'Toggle review flag on selected file'
-);
-_describe(
- Shortcut.REFRESH_CHANGE_LIST,
- ShortcutSection.ACTIONS,
- 'Refresh list of changes'
-);
-_describe(
- Shortcut.TOGGLE_CHANGE_STAR,
- ShortcutSection.ACTIONS,
- 'Star/unstar change'
-);
-_describe(
- Shortcut.OPEN_SUBMIT_DIALOG,
- ShortcutSection.ACTIONS,
- 'Open submit dialog'
-);
-_describe(Shortcut.EDIT_TOPIC, ShortcutSection.ACTIONS, 'Add a change topic');
-_describe(
- Shortcut.DIFF_AGAINST_BASE,
- ShortcutSection.ACTIONS,
- 'Diff against base'
-);
-_describe(
- Shortcut.DIFF_AGAINST_LATEST,
- ShortcutSection.ACTIONS,
- 'Diff against latest patchset'
-);
-_describe(
- Shortcut.DIFF_BASE_AGAINST_LEFT,
- ShortcutSection.ACTIONS,
- 'Diff base against left'
-);
-_describe(
- Shortcut.DIFF_RIGHT_AGAINST_LATEST,
- ShortcutSection.ACTIONS,
- 'Diff right against latest'
-);
-_describe(
- Shortcut.DIFF_BASE_AGAINST_LATEST,
- ShortcutSection.ACTIONS,
- 'Diff base against latest'
-);
-
-_describe(Shortcut.NEXT_LINE, ShortcutSection.DIFFS, 'Go to next line');
-_describe(Shortcut.PREV_LINE, ShortcutSection.DIFFS, 'Go to previous line');
-_describe(
- Shortcut.DIFF_AGAINST_BASE,
- ShortcutSection.DIFFS,
- 'Diff against base'
-);
-_describe(
- Shortcut.DIFF_AGAINST_LATEST,
- ShortcutSection.DIFFS,
- 'Diff against latest patchset'
-);
-_describe(
- Shortcut.DIFF_BASE_AGAINST_LEFT,
- ShortcutSection.DIFFS,
- 'Diff base against left'
-);
-_describe(
- Shortcut.DIFF_RIGHT_AGAINST_LATEST,
- ShortcutSection.DIFFS,
- 'Diff right against latest'
-);
-_describe(
- Shortcut.DIFF_BASE_AGAINST_LATEST,
- ShortcutSection.DIFFS,
- 'Diff base against latest'
-);
-_describe(
- Shortcut.VISIBLE_LINE,
- ShortcutSection.DIFFS,
- 'Move cursor to currently visible code'
-);
-_describe(Shortcut.NEXT_CHUNK, ShortcutSection.DIFFS, 'Go to next diff chunk');
-_describe(
- Shortcut.PREV_CHUNK,
- ShortcutSection.DIFFS,
- 'Go to previous diff chunk'
-);
-_describe(
- Shortcut.TOGGLE_ALL_DIFF_CONTEXT,
- ShortcutSection.DIFFS,
- 'Toggle all diff context'
-);
-_describe(
- Shortcut.NEXT_COMMENT_THREAD,
- ShortcutSection.DIFFS,
- 'Go to next comment thread'
-);
-_describe(
- Shortcut.PREV_COMMENT_THREAD,
- ShortcutSection.DIFFS,
- 'Go to previous comment thread'
-);
-_describe(
- Shortcut.EXPAND_ALL_COMMENT_THREADS,
- ShortcutSection.DIFFS,
- 'Expand all comment threads'
-);
-_describe(
- Shortcut.COLLAPSE_ALL_COMMENT_THREADS,
- ShortcutSection.DIFFS,
- 'Collapse all comment threads'
-);
-_describe(
- Shortcut.TOGGLE_HIDE_ALL_COMMENT_THREADS,
- ShortcutSection.DIFFS,
- 'Hide/Display all comment threads'
-);
-_describe(Shortcut.LEFT_PANE, ShortcutSection.DIFFS, 'Select left pane');
-_describe(Shortcut.RIGHT_PANE, ShortcutSection.DIFFS, 'Select right pane');
-_describe(
- Shortcut.TOGGLE_LEFT_PANE,
- ShortcutSection.DIFFS,
- 'Hide/show left diff'
-);
-_describe(Shortcut.NEW_COMMENT, ShortcutSection.DIFFS, 'Draft new comment');
-_describe(Shortcut.SAVE_COMMENT, ShortcutSection.DIFFS, 'Save comment');
-_describe(
- Shortcut.OPEN_DIFF_PREFS,
- ShortcutSection.DIFFS,
- 'Show diff preferences'
-);
-_describe(
- Shortcut.TOGGLE_DIFF_REVIEWED,
- ShortcutSection.DIFFS,
- 'Mark/unmark file as reviewed'
-);
-_describe(
- Shortcut.TOGGLE_DIFF_MODE,
- ShortcutSection.DIFFS,
- 'Toggle unified/side-by-side diff'
-);
-_describe(
- Shortcut.NEXT_UNREVIEWED_FILE,
- ShortcutSection.DIFFS,
- 'Mark file as reviewed and go to next unreviewed file'
-);
-_describe(Shortcut.TOGGLE_BLAME, ShortcutSection.DIFFS, 'Toggle blame');
-
-_describe(Shortcut.NEXT_FILE, ShortcutSection.NAVIGATION, 'Go to next file');
-_describe(
- Shortcut.PREV_FILE,
- ShortcutSection.NAVIGATION,
- 'Go to previous file'
-);
-_describe(
- Shortcut.NEXT_FILE_WITH_COMMENTS,
- ShortcutSection.NAVIGATION,
- 'Go to next file that has comments'
-);
-_describe(
- Shortcut.PREV_FILE_WITH_COMMENTS,
- ShortcutSection.NAVIGATION,
- 'Go to previous file that has comments'
-);
-_describe(
- Shortcut.OPEN_FIRST_FILE,
- ShortcutSection.NAVIGATION,
- 'Go to first file'
-);
-_describe(
- Shortcut.OPEN_LAST_FILE,
- ShortcutSection.NAVIGATION,
- 'Go to last file'
-);
-_describe(
- Shortcut.UP_TO_DASHBOARD,
- ShortcutSection.NAVIGATION,
- 'Up to dashboard'
-);
-_describe(Shortcut.UP_TO_CHANGE, ShortcutSection.NAVIGATION, 'Up to change');
-
-_describe(
- Shortcut.CURSOR_NEXT_FILE,
- ShortcutSection.FILE_LIST,
- 'Select next file'
-);
-_describe(
- Shortcut.CURSOR_PREV_FILE,
- ShortcutSection.FILE_LIST,
- 'Select previous file'
-);
-_describe(Shortcut.OPEN_FILE, ShortcutSection.FILE_LIST, 'Go to selected file');
-_describe(
- Shortcut.TOGGLE_ALL_INLINE_DIFFS,
- ShortcutSection.FILE_LIST,
- 'Show/hide all inline diffs'
-);
-_describe(
- Shortcut.TOGGLE_HIDE_ALL_COMMENT_THREADS,
- ShortcutSection.FILE_LIST,
- 'Hide/Display all comment threads'
-);
-_describe(
- Shortcut.TOGGLE_INLINE_DIFF,
- ShortcutSection.FILE_LIST,
- 'Show/hide selected inline diff'
-);
-
-_describe(Shortcut.SEND_REPLY, ShortcutSection.REPLY_DIALOG, 'Send reply');
-_describe(
- Shortcut.EMOJI_DROPDOWN,
- ShortcutSection.REPLY_DIALOG,
- 'Emoji dropdown'
-);
-
-/**
- * Shortcut manager, holds all hosts, bindings and listeners.
- */
-export class ShortcutManager {
- private readonly activeHosts = new Map<PolymerElement, Map<string, string>>();
-
- private readonly bindings = new Map<Shortcut, string[]>();
-
- public _testOnly_getBindings() {
- return this.bindings;
- }
-
- public _testOnly_isEmpty() {
- return this.activeHosts.size === 0 && this.listeners.size === 0;
- }
-
- private readonly listeners = new Set<ShortcutListener>();
-
- bindShortcut(shortcut: Shortcut, ...bindings: string[]) {
- this.bindings.set(shortcut, bindings);
- }
-
- getBindingsForShortcut(shortcut: Shortcut) {
- return this.bindings.get(shortcut);
- }
-
- attachHost(host: PolymerElement, shortcuts: Map<string, string>) {
- this.activeHosts.set(host, shortcuts);
- this.notifyListeners();
- }
-
- detachHost(host: PolymerElement) {
- if (this.activeHosts.delete(host)) {
- this.notifyListeners();
- return true;
- }
- return false;
- }
-
- addListener(listener: ShortcutListener) {
- this.listeners.add(listener);
- listener(this.directoryView());
- }
-
- removeListener(listener: ShortcutListener) {
- return this.listeners.delete(listener);
- }
-
- getDescription(section: ShortcutSection, shortcutName: Shortcut) {
- const bindings = _help.get(section);
- let desc = '';
- if (bindings) {
- const binding = bindings.find(
- binding => binding.shortcut === shortcutName
- );
- desc = binding ? binding.text : '';
- }
- return desc;
- }
-
- getShortcut(shortcutName: Shortcut) {
- const bindings = this.bindings.get(shortcutName);
- return bindings
- ? bindings
- .map(binding => this.describeBinding(binding).join('+'))
- .join(',')
- : '';
- }
-
- activeShortcutsBySection() {
- const activeShortcuts = new Set<string>();
- this.activeHosts.forEach(shortcuts => {
- shortcuts.forEach((_, shortcut) => activeShortcuts.add(shortcut));
- });
-
- const activeShortcutsBySection = new Map<
- ShortcutSection,
- ShortcutHelpItem[]
- >();
- _help.forEach((shortcutList, section) => {
- shortcutList.forEach(shortcutHelp => {
- if (activeShortcuts.has(shortcutHelp.shortcut)) {
- if (!activeShortcutsBySection.has(section)) {
- activeShortcutsBySection.set(section, []);
- }
- // From previous condition, the `get(section)`
- // should always return a valid result
- activeShortcutsBySection.get(section)!.push(shortcutHelp);
- }
- });
- });
- return activeShortcutsBySection;
- }
-
- directoryView() {
- const view = new Map<ShortcutSection, SectionView>();
- this.activeShortcutsBySection().forEach((shortcutHelps, section) => {
- const sectionView: Array<{binding: string[][]; text: string}> = [];
- shortcutHelps.forEach(shortcutHelp => {
- const bindingDesc = this.describeBindings(shortcutHelp.shortcut);
- if (!bindingDesc) {
- return;
- }
- this.distributeBindingDesc(bindingDesc).forEach(bindingDesc => {
- sectionView.push({
- binding: bindingDesc,
- text: shortcutHelp.text,
- });
- });
- });
- view.set(section, sectionView);
- });
- return view;
- }
-
- distributeBindingDesc(bindingDesc: string[][]): string[][][] {
- if (
- bindingDesc.length === 1 ||
- this.comboSetDisplayWidth(bindingDesc) < 21
- ) {
- return [bindingDesc];
- }
- // Find the largest prefix of bindings that is under the
- // size threshold.
- const head = [bindingDesc[0]];
- for (let i = 1; i < bindingDesc.length; i++) {
- head.push(bindingDesc[i]);
- if (this.comboSetDisplayWidth(head) >= 21) {
- head.pop();
- return [head].concat(this.distributeBindingDesc(bindingDesc.slice(i)));
- }
- }
- return [];
- }
-
- comboSetDisplayWidth(bindingDesc: string[][]) {
- const bindingSizer = (binding: string[]) =>
- binding.reduce((acc, key) => acc + key.length, 0);
- // Width is the sum of strings + (n-1) * 2 to account for the word
- // "or" joining them.
- return (
- bindingDesc.reduce((acc, binding) => acc + bindingSizer(binding), 0) +
- 2 * (bindingDesc.length - 1)
- );
- }
-
- describeBindings(shortcut: Shortcut): string[][] | null {
- const bindings = this.bindings.get(shortcut);
- if (!bindings) {
- return null;
- }
- if (bindings[0] === SPECIAL_SHORTCUT.GO_KEY) {
- return bindings
- .slice(1)
- .map(binding => this._describeKey(binding))
- .map(binding => ['g'].concat(binding));
- }
- if (bindings[0] === SPECIAL_SHORTCUT.V_KEY) {
- return bindings
- .slice(1)
- .map(binding => this._describeKey(binding))
- .map(binding => ['v'].concat(binding));
- }
-
- return bindings
- .filter(binding => binding !== SPECIAL_SHORTCUT.DOC_ONLY)
- .map(binding => this.describeBinding(binding));
- }
-
- _describeKey(key: string) {
- switch (key) {
- case 'shift':
- return 'Shift';
- case 'meta':
- return 'Meta';
- case 'ctrl':
- return 'Ctrl';
- case 'enter':
- return 'Enter';
- case 'up':
- return '\u2191'; // ↑
- case 'down':
- return '\u2193'; // ↓
- case 'left':
- return '\u2190'; // ←
- case 'right':
- return '\u2192'; // →
- default:
- return key;
- }
- }
-
- describeBinding(binding: string) {
- // single key bindings
- if (binding.length === 1) {
- return [binding];
- }
- return binding
- .split(':')[0]
- .split('+')
- .map(part => this._describeKey(part));
- }
-
- notifyListeners() {
- const view = this.directoryView();
- this.listeners.forEach(listener => listener(view));
- }
-}
-
-const shortcutManager = new ShortcutManager();
-
interface IronA11yKeysMixinConstructor {
// Note: this is needed to have same interface as other mixins
// eslint-disable-next-line @typescript-eslint/no-explicit-any
@@ -773,11 +81,7 @@
ShortcutSection = ShortcutSection;
- private _disableKeyboardShortcuts = false;
-
- private readonly restApiService = appContext.restApiService;
-
- private reporting = appContext.reportingService;
+ private readonly shortcuts = appContext.shortcutsService;
/** Used to disable shortcuts when the element is not visible. */
private observer?: IntersectionObserver;
@@ -796,76 +100,18 @@
/** Are shortcuts currently enabled? True only when element is visible. */
private bindingsEnabled = false;
- modifierPressed(event: CustomKeyboardEvent) {
+ modifierPressed(e: IronKeyboardEvent) {
/* We are checking for g/v as modifiers pressed. There are cases such as
* pressing v and then /, where we want the handler for / to be triggered.
* TODO(dhruvsri): find a way to support that keyboard combination
*/
- const e = getKeyboardEvent(event);
return (
isModifierPressed(e) || !!this._inGoKeyMode() || !!this.inVKeyMode()
);
}
- shouldSuppressKeyboardShortcut(event: CustomKeyboardEvent) {
- if (this._disableKeyboardShortcuts) return true;
- const e = getKeyboardEvent(event);
- // TODO(TS): maybe override the EventApi, narrow it down to Element always
- const target = (dom(e) as EventApi).rootTarget as Element;
- const tagName = target.tagName;
- const type = target.getAttribute('type');
- if (
- // Suppress shortcuts on <input> and <textarea>, but not on
- // checkboxes, because we want to enable workflows like 'click
- // mark-reviewed and then press ] to go to the next file'.
- (tagName === 'INPUT' && type !== 'checkbox') ||
- tagName === 'TEXTAREA' ||
- // Suppress shortcuts if the key is 'enter'
- // and target is an anchor or button or paper-tab.
- (e.keyCode === 13 &&
- (tagName === 'A' || tagName === 'BUTTON' || tagName === 'PAPER-TAB'))
- ) {
- return true;
- }
- for (let i = 0; e.path && i < e.path.length; i++) {
- // TODO(TS): narrow this down to Element from EventTarget first
- if ((e.path[i] as Element).tagName === 'GR-OVERLAY') {
- return true;
- }
- }
-
- // eg: {key: "k:keydown", ..., from: "gr-diff-view"}
- let key = `${(e as unknown as KeyboardEvent).key}:${e.type}`;
- if (this._inGoKeyMode()) key = 'g+' + key;
- if (this.inVKeyMode()) key = 'v+' + key;
- if (e.shiftKey) key = 'shift+' + key;
- if (e.ctrlKey) key = 'ctrl+' + key;
- if (e.metaKey) key = 'meta+' + key;
- if (e.altKey) key = 'alt+' + key;
- this.reporting.reportInteraction('shortcut-triggered', {
- key,
- from: this.nodeName ?? 'unknown',
- });
- return false;
- }
-
- // Alias for getKeyboardEvent.
- getKeyboardEvent(e: CustomKeyboardEvent) {
- return getKeyboardEvent(e);
- }
-
- bindShortcut(shortcut: Shortcut, ...bindings: string[]) {
- shortcutManager.bindShortcut(shortcut, ...bindings);
- }
-
- createTitle(shortcutName: Shortcut, section: ShortcutSection) {
- const desc = shortcutManager.getDescription(section, shortcutName);
- const shortcut = shortcutManager.getShortcut(shortcutName);
- return desc && shortcut ? `${desc} (shortcut: ${shortcut})` : '';
- }
-
_addOwnKeyBindings(shortcut: Shortcut, handler: string) {
- const bindings = shortcutManager.getBindingsForShortcut(shortcut);
+ const bindings = this.shortcuts.getBindingsForShortcut(shortcut);
if (!bindings) {
return;
}
@@ -890,11 +136,6 @@
override connectedCallback() {
super.connectedCallback();
- this.restApiService.getPreferences().then(prefs => {
- if (prefs?.disable_keyboard_shortcuts) {
- this._disableKeyboardShortcuts = true;
- }
- });
this.createVisibilityObserver();
this.enableBindings();
}
@@ -941,7 +182,7 @@
const shortcuts = new Map<string, string>(
Object.entries(this.keyboardShortcuts())
);
- shortcutManager.attachHost(this, shortcuts);
+ this.shortcuts.attachHost(this, shortcuts);
for (const [key, value] of shortcuts.entries()) {
this._addOwnKeyBindings(key as Shortcut, value);
@@ -977,7 +218,7 @@
private disableBindings() {
if (!this.bindingsEnabled) return;
this.bindingsEnabled = false;
- if (shortcutManager.detachHost(this)) {
+ if (this.shortcuts.detachHost(this)) {
this.removeOwnKeyBindings();
}
}
@@ -990,16 +231,8 @@
return {};
}
- addKeyboardShortcutDirectoryListener(listener: ShortcutListener) {
- shortcutManager.addListener(listener);
- }
-
- removeKeyboardShortcutDirectoryListener(listener: ShortcutListener) {
- shortcutManager.removeListener(listener);
- }
-
- _handleVKeyDown(e: CustomKeyboardEvent) {
- if (this.shouldSuppressKeyboardShortcut(e)) return;
+ _handleVKeyDown(e: IronKeyboardEvent) {
+ if (this.shortcuts.shouldSuppress(e)) return;
this._shortcut_v_key_last_pressed = Date.now();
}
@@ -1016,11 +249,11 @@
);
}
- _handleVAction(e: CustomKeyboardEvent) {
+ _handleVAction(e: IronKeyboardEvent) {
if (
!this.inVKeyMode() ||
!this._shortcut_v_table.has(e.detail.key) ||
- this.shouldSuppressKeyboardShortcut(e)
+ this.shortcuts.shouldSuppress(e)
) {
return;
}
@@ -1033,8 +266,8 @@
}
}
- _handleGoKeyDown(e: CustomKeyboardEvent) {
- if (this.shouldSuppressKeyboardShortcut(e)) return;
+ _handleGoKeyDown(e: IronKeyboardEvent) {
+ if (this.shortcuts.shouldSuppress(e)) return;
this._shortcut_go_key_last_pressed = Date.now();
}
@@ -1053,11 +286,11 @@
);
}
- _handleGoAction(e: CustomKeyboardEvent) {
+ _handleGoAction(e: IronKeyboardEvent) {
if (
!this._inGoKeyMode() ||
!this._shortcut_go_table.has(e.detail.key) ||
- this.shouldSuppressKeyboardShortcut(e)
+ this.shortcuts.shouldSuppress(e)
) {
return;
}
@@ -1071,7 +304,10 @@
}
}
- return Mixin as T & Constructor<KeyboardShortcutMixinInterface>;
+ return Mixin as T &
+ Constructor<
+ KeyboardShortcutMixinInterface & KeyboardShortcutMixinInterfaceTesting
+ >;
};
// The following doesn't work (IronA11yKeysBehavior crashes):
@@ -1084,7 +320,10 @@
// This is a workaround
export const KeyboardShortcutMixin = <T extends Constructor<PolymerElement>>(
superClass: T
-): T & Constructor<KeyboardShortcutMixinInterface> =>
+): T &
+ Constructor<
+ KeyboardShortcutMixinInterface & KeyboardShortcutMixinInterfaceTesting
+ > =>
InternalKeyboardShortcutMixin(
// TODO(TS): mixinBehaviors in some lib is returning: `new () => T` instead
// which will fail the type check due to missing IronA11yKeysBehavior interface
@@ -1094,21 +333,14 @@
/** The interface corresponding to KeyboardShortcutMixin */
export interface KeyboardShortcutMixinInterface {
- Shortcut: typeof Shortcut;
- ShortcutSection: typeof ShortcutSection;
+ keyboardShortcuts(): {[key: string]: string | null};
+ modifierPressed(event: IronKeyboardEvent): boolean;
+}
+
+export interface KeyboardShortcutMixinInterfaceTesting {
_shortcut_go_key_last_pressed: number | null;
_shortcut_v_key_last_pressed: number | null;
_shortcut_go_table: Map<string, string>;
_shortcut_v_table: Map<string, string>;
- keyboardShortcuts(): {[key: string]: string | null};
- createTitle(name: Shortcut, section: ShortcutSection): string;
- bindShortcut(shortcut: Shortcut, ...bindings: string[]): void;
- shouldSuppressKeyboardShortcut(event: CustomKeyboardEvent): boolean;
- modifierPressed(event: CustomKeyboardEvent): boolean;
- addKeyboardShortcutDirectoryListener(listener: ShortcutListener): void;
- removeKeyboardShortcutDirectoryListener(listener: ShortcutListener): void;
-}
-
-export function _testOnly_getShortcutManagerInstance() {
- return shortcutManager;
+ _handleGoAction: (e: IronKeyboardEvent) => void;
}
diff --git a/polygerrit-ui/app/mixins/keyboard-shortcut-mixin/keyboard-shortcut-mixin_test.js b/polygerrit-ui/app/mixins/keyboard-shortcut-mixin/keyboard-shortcut-mixin_test.js
deleted file mode 100644
index 4536ecd..0000000
--- a/polygerrit-ui/app/mixins/keyboard-shortcut-mixin/keyboard-shortcut-mixin_test.js
+++ /dev/null
@@ -1,424 +0,0 @@
-/**
- * @license
- * Copyright (C) 2016 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 {
- KeyboardShortcutMixin, Shortcut,
- ShortcutManager, ShortcutSection, SPECIAL_SHORTCUT,
-} from './keyboard-shortcut-mixin.js';
-import {html} from '@polymer/polymer/lib/utils/html-tag.js';
-import {PolymerElement} from '@polymer/polymer/polymer-element.js';
-import {mockPromise} from '../../test/test-utils.js';
-
-const basicFixture =
- fixtureFromElement('keyboard-shortcut-mixin-test-element');
-
-const withinOverlayFixture = fixtureFromTemplate(html`
-<gr-overlay>
- <keyboard-shortcut-mixin-test-element>
- </keyboard-shortcut-mixin-test-element>
-</gr-overlay>
-`);
-
-class GrKeyboardShortcutMixinTestElement extends
- KeyboardShortcutMixin(PolymerElement) {
- static get is() {
- return 'keyboard-shortcut-mixin-test-element';
- }
-
- get keyBindings() {
- return {
- k: '_handleKey',
- enter: '_handleKey',
- };
- }
-
- _handleKey() {}
-}
-
-customElements.define(GrKeyboardShortcutMixinTestElement.is,
- GrKeyboardShortcutMixinTestElement);
-
-suite('keyboard-shortcut-mixin tests', () => {
- let element;
- let overlay;
-
- setup(() => {
- element = basicFixture.instantiate();
- overlay = withinOverlayFixture.instantiate();
- });
-
- suite('ShortcutManager', () => {
- test('bindings management', () => {
- const mgr = new ShortcutManager();
- const NEXT_FILE = Shortcut.NEXT_FILE;
-
- assert.isUndefined(mgr.getBindingsForShortcut(NEXT_FILE));
- mgr.bindShortcut(NEXT_FILE, ']', '}', 'right');
- assert.deepEqual(
- mgr.getBindingsForShortcut(NEXT_FILE),
- [']', '}', 'right']);
- });
-
- test('getShortcut', () => {
- const mgr = new ShortcutManager();
- const NEXT_FILE = Shortcut.NEXT_FILE;
-
- assert.isUndefined(mgr.getBindingsForShortcut(NEXT_FILE));
- mgr.bindShortcut(NEXT_FILE, ']', '}', 'right');
- assert.equal(mgr.getShortcut(NEXT_FILE), '],},→');
- });
-
- test('getShortcut with modifiers', () => {
- const mgr = new ShortcutManager();
- const NEXT_FILE = Shortcut.NEXT_FILE;
-
- assert.isUndefined(mgr.getBindingsForShortcut(NEXT_FILE));
- mgr.bindShortcut(NEXT_FILE, 'Shift+a:key');
- assert.equal(mgr.getShortcut(NEXT_FILE), 'Shift+a');
- });
-
- suite('binding descriptions', () => {
- function mapToObject(m) {
- const o = {};
- m.forEach((v, k) => o[k] = v);
- return o;
- }
-
- test('single combo description', () => {
- const mgr = new ShortcutManager();
- assert.deepEqual(mgr.describeBinding('a'), ['a']);
- assert.deepEqual(mgr.describeBinding('a:keyup'), ['a']);
- assert.deepEqual(mgr.describeBinding('ctrl+a'), ['Ctrl', 'a']);
- assert.deepEqual(
- mgr.describeBinding('ctrl+shift+up:keyup'),
- ['Ctrl', 'Shift', '↑']);
- });
-
- test('combo set description', () => {
- const mgr = new ShortcutManager();
- assert.isNull(mgr.describeBindings(Shortcut.NEXT_FILE));
-
- mgr.bindShortcut(Shortcut.GO_TO_OPENED_CHANGES,
- SPECIAL_SHORTCUT.GO_KEY, 'o');
- assert.deepEqual(
- mgr.describeBindings(Shortcut.GO_TO_OPENED_CHANGES),
- [['g', 'o']]);
-
- mgr.bindShortcut(Shortcut.NEXT_FILE, SPECIAL_SHORTCUT.DOC_ONLY,
- ']', 'ctrl+shift+right:keyup');
- assert.deepEqual(
- mgr.describeBindings(Shortcut.NEXT_FILE),
- [[']'], ['Ctrl', 'Shift', '→']]);
-
- mgr.bindShortcut(Shortcut.PREV_FILE, '[');
- assert.deepEqual(mgr.describeBindings(Shortcut.PREV_FILE), [['[']]);
- });
-
- test('combo set description width', () => {
- const mgr = new ShortcutManager();
- assert.strictEqual(mgr.comboSetDisplayWidth([['u']]), 1);
- assert.strictEqual(mgr.comboSetDisplayWidth([['g', 'o']]), 2);
- assert.strictEqual(mgr.comboSetDisplayWidth([['Shift', 'r']]), 6);
- assert.strictEqual(mgr.comboSetDisplayWidth([['x'], ['y']]), 4);
- assert.strictEqual(
- mgr.comboSetDisplayWidth([['x'], ['y'], ['Shift', 'z']]),
- 12);
- });
-
- test('distribute shortcut help', () => {
- const mgr = new ShortcutManager();
- assert.deepEqual(mgr.distributeBindingDesc([['o']]), [[['o']]]);
- assert.deepEqual(
- mgr.distributeBindingDesc([['g', 'o']]),
- [[['g', 'o']]]);
- assert.deepEqual(
- mgr.distributeBindingDesc([['ctrl', 'shift', 'meta', 'enter']]),
- [[['ctrl', 'shift', 'meta', 'enter']]]);
- assert.deepEqual(
- mgr.distributeBindingDesc([
- ['ctrl', 'shift', 'meta', 'enter'],
- ['o'],
- ]),
- [
- [['ctrl', 'shift', 'meta', 'enter']],
- [['o']],
- ]);
- assert.deepEqual(
- mgr.distributeBindingDesc([
- ['ctrl', 'enter'],
- ['meta', 'enter'],
- ['ctrl', 's'],
- ['meta', 's'],
- ]),
- [
- [['ctrl', 'enter'], ['meta', 'enter']],
- [['ctrl', 's'], ['meta', 's']],
- ]);
- });
-
- test('active shortcuts by section', () => {
- const mgr = new ShortcutManager();
- mgr.bindShortcut(Shortcut.NEXT_FILE, ']');
- mgr.bindShortcut(Shortcut.NEXT_LINE, 'j');
- mgr.bindShortcut(Shortcut.GO_TO_OPENED_CHANGES, 'g+o');
- mgr.bindShortcut(Shortcut.SEARCH, '/');
-
- assert.deepEqual(
- mapToObject(mgr.activeShortcutsBySection()),
- {});
-
- mgr.attachHost({}, new Map([[Shortcut.NEXT_FILE, null]]));
- assert.deepEqual(
- mapToObject(mgr.activeShortcutsBySection()),
- {
- [ShortcutSection.NAVIGATION]: [
- {shortcut: Shortcut.NEXT_FILE, text: 'Go to next file'},
- ],
- });
-
- mgr.attachHost({}, new Map([[Shortcut.NEXT_LINE, null]]));
- assert.deepEqual(
- mapToObject(mgr.activeShortcutsBySection()),
- {
- [ShortcutSection.DIFFS]: [
- {shortcut: Shortcut.NEXT_LINE, text: 'Go to next line'},
- ],
- [ShortcutSection.NAVIGATION]: [
- {shortcut: Shortcut.NEXT_FILE, text: 'Go to next file'},
- ],
- });
-
- mgr.attachHost({}, new Map([
- [Shortcut.SEARCH, null],
- [Shortcut.GO_TO_OPENED_CHANGES, null],
- ]));
- assert.deepEqual(
- mapToObject(mgr.activeShortcutsBySection()),
- {
- [ShortcutSection.DIFFS]: [
- {shortcut: Shortcut.NEXT_LINE, text: 'Go to next line'},
- ],
- [ShortcutSection.EVERYWHERE]: [
- {shortcut: Shortcut.SEARCH, text: 'Search'},
- {
- shortcut: Shortcut.GO_TO_OPENED_CHANGES,
- text: 'Go to Opened Changes',
- },
- ],
- [ShortcutSection.NAVIGATION]: [
- {shortcut: Shortcut.NEXT_FILE, text: 'Go to next file'},
- ],
- });
- });
-
- test('directory view', () => {
- const mgr = new ShortcutManager();
- mgr.bindShortcut(Shortcut.NEXT_FILE, ']');
- mgr.bindShortcut(Shortcut.NEXT_LINE, 'j');
- mgr.bindShortcut(Shortcut.GO_TO_OPENED_CHANGES,
- SPECIAL_SHORTCUT.GO_KEY, 'o');
- mgr.bindShortcut(Shortcut.SEARCH, '/');
- mgr.bindShortcut(
- Shortcut.SAVE_COMMENT, 'ctrl+enter', 'meta+enter',
- 'ctrl+s', 'meta+s');
-
- assert.deepEqual(mapToObject(mgr.directoryView()), {});
-
- mgr.attachHost({}, new Map([
- [Shortcut.GO_TO_OPENED_CHANGES, null],
- [Shortcut.NEXT_FILE, null],
- [Shortcut.NEXT_LINE, null],
- [Shortcut.SAVE_COMMENT, null],
- [Shortcut.SEARCH, null],
- ]));
- assert.deepEqual(
- mapToObject(mgr.directoryView()),
- {
- [ShortcutSection.DIFFS]: [
- {binding: [['j']], text: 'Go to next line'},
- {
- binding: [['Ctrl', 'Enter'], ['Meta', 'Enter']],
- text: 'Save comment',
- },
- {
- binding: [['Ctrl', 's'], ['Meta', 's']],
- text: 'Save comment',
- },
- ],
- [ShortcutSection.EVERYWHERE]: [
- {binding: [['/']], text: 'Search'},
- {binding: [['g', 'o']], text: 'Go to Opened Changes'},
- ],
- [ShortcutSection.NAVIGATION]: [
- {binding: [[']']], text: 'Go to next file'},
- ],
- });
- });
- });
- });
-
- test('doesn’t block kb shortcuts for non-allowed els', async () => {
- const divEl = document.createElement('div');
- element.appendChild(divEl);
- const promise = mockPromise();
- element._handleKey = e => {
- assert.isFalse(element.shouldSuppressKeyboardShortcut(e));
- promise.resolve();
- };
- MockInteractions.keyDownOn(divEl, 75, null, 'k');
- await promise;
- });
-
- test('blocks kb shortcuts for input els', async () => {
- const inputEl = document.createElement('input');
- element.appendChild(inputEl);
- const promise = mockPromise();
- element._handleKey = e => {
- assert.isTrue(element.shouldSuppressKeyboardShortcut(e));
- promise.resolve();
- };
- MockInteractions.keyDownOn(inputEl, 75, null, 'k');
- await promise;
- });
-
- test('doesn’t block kb shortcuts for checkboxes', async () => {
- const inputEl = document.createElement('input');
- inputEl.setAttribute('type', 'checkbox');
- element.appendChild(inputEl);
- const promise = mockPromise();
- element._handleKey = e => {
- assert.isFalse(element.shouldSuppressKeyboardShortcut(e));
- promise.resolve();
- };
- MockInteractions.keyDownOn(inputEl, 75, null, 'k');
- await promise;
- });
-
- test('blocks kb shortcuts for textarea els', async () => {
- const textareaEl = document.createElement('textarea');
- element.appendChild(textareaEl);
- const promise = mockPromise();
- element._handleKey = e => {
- assert.isTrue(element.shouldSuppressKeyboardShortcut(e));
- promise.resolve();
- };
- MockInteractions.keyDownOn(textareaEl, 75, null, 'k');
- await promise;
- });
-
- test('blocks kb shortcuts for anything in a gr-overlay', async () => {
- const divEl = document.createElement('div');
- const element =
- overlay.querySelector('keyboard-shortcut-mixin-test-element');
- element.appendChild(divEl);
- const promise = mockPromise();
- element._handleKey = e => {
- assert.isTrue(element.shouldSuppressKeyboardShortcut(e));
- promise.resolve();
- };
- MockInteractions.keyDownOn(divEl, 75, null, 'k');
- await promise;
- });
-
- test('blocks enter shortcut on an anchor', async () => {
- const anchorEl = document.createElement('a');
- const element =
- overlay.querySelector('keyboard-shortcut-mixin-test-element');
- element.appendChild(anchorEl);
- const promise = mockPromise();
- element._handleKey = e => {
- assert.isTrue(element.shouldSuppressKeyboardShortcut(e));
- promise.resolve();
- };
- MockInteractions.keyDownOn(anchorEl, 13, null, 'enter');
- await promise;
- });
-
- test('modifierPressed returns accurate values', () => {
- const spy = sinon.spy(element, 'modifierPressed');
- element._handleKey = e => {
- element.modifierPressed(e);
- };
- MockInteractions.keyDownOn(element, 75, 'shift', 'k');
- assert.isTrue(spy.lastCall.returnValue);
- MockInteractions.keyDownOn(element, 75, null, 'k');
- assert.isFalse(spy.lastCall.returnValue);
- MockInteractions.keyDownOn(element, 75, 'ctrl', 'k');
- assert.isTrue(spy.lastCall.returnValue);
- MockInteractions.keyDownOn(element, 75, null, 'k');
- assert.isFalse(spy.lastCall.returnValue);
- MockInteractions.keyDownOn(element, 75, 'meta', 'k');
- assert.isTrue(spy.lastCall.returnValue);
- MockInteractions.keyDownOn(element, 75, null, 'k');
- assert.isFalse(spy.lastCall.returnValue);
- MockInteractions.keyDownOn(element, 75, 'alt', 'k');
- assert.isTrue(spy.lastCall.returnValue);
- });
-
- suite('GO_KEY timing', () => {
- let handlerStub;
-
- setup(() => {
- element._shortcut_go_table.set('a', '_handleA');
- handlerStub = element._handleA = sinon.stub();
- sinon.stub(Date, 'now').returns(10000);
- });
-
- test('success', () => {
- const e = {detail: {key: 'a'}, preventDefault: () => {}};
- sinon.stub(element, 'shouldSuppressKeyboardShortcut').returns(false);
- element._shortcut_go_key_last_pressed = 9000;
- element._handleGoAction(e);
- assert.isTrue(handlerStub.calledOnce);
- assert.strictEqual(handlerStub.lastCall.args[0], e);
- });
-
- test('go key not pressed', () => {
- const e = {detail: {key: 'a'}, preventDefault: () => {}};
- sinon.stub(element, 'shouldSuppressKeyboardShortcut').returns(false);
- element._shortcut_go_key_last_pressed = null;
- element._handleGoAction(e);
- assert.isFalse(handlerStub.called);
- });
-
- test('go key pressed too long ago', () => {
- const e = {detail: {key: 'a'}, preventDefault: () => {}};
- sinon.stub(element, 'shouldSuppressKeyboardShortcut').returns(false);
- element._shortcut_go_key_last_pressed = 3000;
- element._handleGoAction(e);
- assert.isFalse(handlerStub.called);
- });
-
- test('should suppress', () => {
- const e = {detail: {key: 'a'}, preventDefault: () => {}};
- sinon.stub(element, 'shouldSuppressKeyboardShortcut').returns(true);
- element._shortcut_go_key_last_pressed = 9000;
- element._handleGoAction(e);
- assert.isFalse(handlerStub.called);
- });
-
- test('unrecognized key', () => {
- const e = {detail: {key: 'f'}, preventDefault: () => {}};
- sinon.stub(element, 'shouldSuppressKeyboardShortcut').returns(false);
- element._shortcut_go_key_last_pressed = 9000;
- element._handleGoAction(e);
- assert.isFalse(handlerStub.called);
- });
- });
-});
-
diff --git a/polygerrit-ui/app/mixins/keyboard-shortcut-mixin/keyboard-shortcut-mixin_test.ts b/polygerrit-ui/app/mixins/keyboard-shortcut-mixin/keyboard-shortcut-mixin_test.ts
new file mode 100644
index 0000000..01ad6cc
--- /dev/null
+++ b/polygerrit-ui/app/mixins/keyboard-shortcut-mixin/keyboard-shortcut-mixin_test.ts
@@ -0,0 +1,139 @@
+/**
+ * @license
+ * Copyright (C) 2016 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';
+import {KeyboardShortcutMixin} from './keyboard-shortcut-mixin';
+import {PolymerElement} from '@polymer/polymer/polymer-element';
+import '../../elements/shared/gr-overlay/gr-overlay';
+import * as MockInteractions from '@polymer/iron-test-helpers/mock-interactions';
+import {IronKeyboardEvent} from '../../types/events';
+
+class GrKeyboardShortcutMixinTestElement extends KeyboardShortcutMixin(
+ PolymerElement
+) {
+ static get is() {
+ return 'keyboard-shortcut-mixin-test-element';
+ }
+
+ get keyBindings() {
+ return {
+ k: '_handleKey',
+ enter: '_handleKey',
+ };
+ }
+
+ _handleKey(_: any) {}
+
+ _handleA(_: any) {}
+}
+
+declare global {
+ interface HTMLElementTagNameMap {
+ 'keyboard-shortcut-mixin-test-element': GrKeyboardShortcutMixinTestElement;
+ }
+}
+
+customElements.define(
+ GrKeyboardShortcutMixinTestElement.is,
+ GrKeyboardShortcutMixinTestElement
+);
+
+const basicFixture = fixtureFromElement('keyboard-shortcut-mixin-test-element');
+
+suite('keyboard-shortcut-mixin tests', () => {
+ let element: GrKeyboardShortcutMixinTestElement;
+
+ setup(async () => {
+ element = basicFixture.instantiate();
+ await flush();
+ });
+
+ test('modifierPressed returns accurate values', () => {
+ const spy = sinon.spy(element, 'modifierPressed');
+ element._handleKey = e => {
+ element.modifierPressed(e);
+ };
+ MockInteractions.keyDownOn(element, 75, 'shift', 'k');
+ assert.isTrue(spy.lastCall.returnValue);
+ MockInteractions.keyDownOn(element, 75, null, 'k');
+ assert.isFalse(spy.lastCall.returnValue);
+ MockInteractions.keyDownOn(element, 75, 'ctrl', 'k');
+ assert.isTrue(spy.lastCall.returnValue);
+ MockInteractions.keyDownOn(element, 75, null, 'k');
+ assert.isFalse(spy.lastCall.returnValue);
+ MockInteractions.keyDownOn(element, 75, 'meta', 'k');
+ assert.isTrue(spy.lastCall.returnValue);
+ MockInteractions.keyDownOn(element, 75, null, 'k');
+ assert.isFalse(spy.lastCall.returnValue);
+ MockInteractions.keyDownOn(element, 75, 'alt', 'k');
+ assert.isTrue(spy.lastCall.returnValue);
+ });
+
+ suite('GO_KEY timing', () => {
+ let handlerStub: sinon.SinonStub;
+
+ setup(() => {
+ element._shortcut_go_table.set('a', '_handleA');
+ handlerStub = element._handleA = sinon.stub();
+ sinon.stub(Date, 'now').returns(10000);
+ });
+
+ test('success', () => {
+ const e = {
+ detail: {key: 'a'},
+ preventDefault: () => {},
+ composedPath: () => [],
+ } as unknown as IronKeyboardEvent;
+ element._shortcut_go_key_last_pressed = 9000;
+ element._handleGoAction(e);
+ assert.isTrue(handlerStub.calledOnce);
+ assert.strictEqual(handlerStub.lastCall.args[0], e);
+ });
+
+ test('go key not pressed', () => {
+ const e = {
+ detail: {key: 'a'},
+ preventDefault: () => {},
+ composedPath: () => [],
+ } as unknown as IronKeyboardEvent;
+ element._shortcut_go_key_last_pressed = null;
+ element._handleGoAction(e);
+ assert.isFalse(handlerStub.called);
+ });
+
+ test('go key pressed too long ago', () => {
+ const e = {
+ detail: {key: 'a'},
+ preventDefault: () => {},
+ composedPath: () => [],
+ } as unknown as IronKeyboardEvent;
+ element._shortcut_go_key_last_pressed = 3000;
+ element._handleGoAction(e);
+ assert.isFalse(handlerStub.called);
+ });
+
+ test('unrecognized key', () => {
+ const e = {
+ detail: {key: 'f'},
+ preventDefault: () => {},
+ composedPath: () => [],
+ } as unknown as IronKeyboardEvent;
+ element._shortcut_go_key_last_pressed = 9000;
+ element._handleGoAction(e);
+ assert.isFalse(handlerStub.called);
+ });
+ });
+});
diff --git a/polygerrit-ui/app/package.json b/polygerrit-ui/app/package.json
index 4be6241..2ad4e79 100644
--- a/polygerrit-ui/app/package.json
+++ b/polygerrit-ui/app/package.json
@@ -38,7 +38,7 @@
"ba-linkify": "^1.0.1",
"codemirror-minified": "^5.62.2",
"immer": "^9.0.5",
- "lit": "2.0.0-rc.3",
+ "lit": "2.0.2",
"page": "^1.11.6",
"polymer-bridges": "file:../../polymer-bridges/",
"polymer-resin": "^2.0.1",
diff --git a/polygerrit-ui/app/services/app-context-init.ts b/polygerrit-ui/app/services/app-context-init.ts
index ade9529..3a6f7c5 100644
--- a/polygerrit-ui/app/services/app-context-init.ts
+++ b/polygerrit-ui/app/services/app-context-init.ts
@@ -27,6 +27,7 @@
import {ConfigService} from './config/config-service';
import {UserService} from './user/user-service';
import {CommentsService} from './comments/comments-service';
+import {ShortcutsService} from './shortcuts/shortcuts-service';
type ServiceName = keyof AppContext;
type ServiceCreator<T> = () => T;
@@ -82,5 +83,6 @@
storageService: () => new GrStorageService(),
configService: () => new ConfigService(),
userService: () => new UserService(appContext.restApiService),
+ shortcutsService: () => new ShortcutsService(appContext.reportingService),
});
}
diff --git a/polygerrit-ui/app/services/app-context.ts b/polygerrit-ui/app/services/app-context.ts
index 161378d..e5828d6 100644
--- a/polygerrit-ui/app/services/app-context.ts
+++ b/polygerrit-ui/app/services/app-context.ts
@@ -26,6 +26,7 @@
import {ConfigService} from './config/config-service';
import {UserService} from './user/user-service';
import {CommentsService} from './comments/comments-service';
+import {ShortcutsService} from './shortcuts/shortcuts-service';
export interface AppContext {
flagsService: FlagsService;
@@ -40,6 +41,7 @@
storageService: StorageService;
configService: ConfigService;
userService: UserService;
+ shortcutsService: ShortcutsService;
}
/**
diff --git a/polygerrit-ui/app/services/shortcuts/shortcuts-config.ts b/polygerrit-ui/app/services/shortcuts/shortcuts-config.ts
new file mode 100644
index 0000000..bd004d7
--- /dev/null
+++ b/polygerrit-ui/app/services/shortcuts/shortcuts-config.ts
@@ -0,0 +1,552 @@
+/**
+ * @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.
+ */
+
+/** Enum for all special shortcuts */
+export enum SPECIAL_SHORTCUT {
+ DOC_ONLY = 'DOC_ONLY',
+ GO_KEY = 'GO_KEY',
+ V_KEY = 'V_KEY',
+}
+
+/**
+ * Enum for all shortcut sections, where that shortcut should be applied to.
+ */
+export enum ShortcutSection {
+ ACTIONS = 'Actions',
+ DIFFS = 'Diffs',
+ EVERYWHERE = 'Global Shortcuts',
+ FILE_LIST = 'File list',
+ NAVIGATION = 'Navigation',
+ REPLY_DIALOG = 'Reply dialog',
+}
+
+/**
+ * Enum for all possible shortcut names.
+ */
+export enum Shortcut {
+ OPEN_SHORTCUT_HELP_DIALOG = 'OPEN_SHORTCUT_HELP_DIALOG',
+ GO_TO_USER_DASHBOARD = 'GO_TO_USER_DASHBOARD',
+ GO_TO_OPENED_CHANGES = 'GO_TO_OPENED_CHANGES',
+ GO_TO_MERGED_CHANGES = 'GO_TO_MERGED_CHANGES',
+ GO_TO_ABANDONED_CHANGES = 'GO_TO_ABANDONED_CHANGES',
+ GO_TO_WATCHED_CHANGES = 'GO_TO_WATCHED_CHANGES',
+
+ CURSOR_NEXT_CHANGE = 'CURSOR_NEXT_CHANGE',
+ CURSOR_PREV_CHANGE = 'CURSOR_PREV_CHANGE',
+ OPEN_CHANGE = 'OPEN_CHANGE',
+ NEXT_PAGE = 'NEXT_PAGE',
+ PREV_PAGE = 'PREV_PAGE',
+ TOGGLE_CHANGE_REVIEWED = 'TOGGLE_CHANGE_REVIEWED',
+ TOGGLE_CHANGE_STAR = 'TOGGLE_CHANGE_STAR',
+ REFRESH_CHANGE_LIST = 'REFRESH_CHANGE_LIST',
+ OPEN_SUBMIT_DIALOG = 'OPEN_SUBMIT_DIALOG',
+ TOGGLE_ATTENTION_SET = 'TOGGLE_ATTENTION_SET',
+
+ OPEN_REPLY_DIALOG = 'OPEN_REPLY_DIALOG',
+ OPEN_DOWNLOAD_DIALOG = 'OPEN_DOWNLOAD_DIALOG',
+ EXPAND_ALL_MESSAGES = 'EXPAND_ALL_MESSAGES',
+ COLLAPSE_ALL_MESSAGES = 'COLLAPSE_ALL_MESSAGES',
+ UP_TO_DASHBOARD = 'UP_TO_DASHBOARD',
+ UP_TO_CHANGE = 'UP_TO_CHANGE',
+ TOGGLE_DIFF_MODE = 'TOGGLE_DIFF_MODE',
+ REFRESH_CHANGE = 'REFRESH_CHANGE',
+ EDIT_TOPIC = 'EDIT_TOPIC',
+ DIFF_AGAINST_BASE = 'DIFF_AGAINST_BASE',
+ DIFF_AGAINST_LATEST = 'DIFF_AGAINST_LATEST',
+ DIFF_BASE_AGAINST_LEFT = 'DIFF_BASE_AGAINST_LEFT',
+ DIFF_RIGHT_AGAINST_LATEST = 'DIFF_RIGHT_AGAINST_LATEST',
+ DIFF_BASE_AGAINST_LATEST = 'DIFF_BASE_AGAINST_LATEST',
+
+ NEXT_LINE = 'NEXT_LINE',
+ PREV_LINE = 'PREV_LINE',
+ VISIBLE_LINE = 'VISIBLE_LINE',
+ NEXT_CHUNK = 'NEXT_CHUNK',
+ PREV_CHUNK = 'PREV_CHUNK',
+ TOGGLE_ALL_DIFF_CONTEXT = 'TOGGLE_ALL_DIFF_CONTEXT',
+ NEXT_COMMENT_THREAD = 'NEXT_COMMENT_THREAD',
+ PREV_COMMENT_THREAD = 'PREV_COMMENT_THREAD',
+ EXPAND_ALL_COMMENT_THREADS = 'EXPAND_ALL_COMMENT_THREADS',
+ COLLAPSE_ALL_COMMENT_THREADS = 'COLLAPSE_ALL_COMMENT_THREADS',
+ LEFT_PANE = 'LEFT_PANE',
+ RIGHT_PANE = 'RIGHT_PANE',
+ TOGGLE_LEFT_PANE = 'TOGGLE_LEFT_PANE',
+ NEW_COMMENT = 'NEW_COMMENT',
+ SAVE_COMMENT = 'SAVE_COMMENT',
+ OPEN_DIFF_PREFS = 'OPEN_DIFF_PREFS',
+ TOGGLE_DIFF_REVIEWED = 'TOGGLE_DIFF_REVIEWED',
+
+ NEXT_FILE = 'NEXT_FILE',
+ PREV_FILE = 'PREV_FILE',
+ NEXT_FILE_WITH_COMMENTS = 'NEXT_FILE_WITH_COMMENTS',
+ PREV_FILE_WITH_COMMENTS = 'PREV_FILE_WITH_COMMENTS',
+ NEXT_UNREVIEWED_FILE = 'NEXT_UNREVIEWED_FILE',
+ CURSOR_NEXT_FILE = 'CURSOR_NEXT_FILE',
+ CURSOR_PREV_FILE = 'CURSOR_PREV_FILE',
+ OPEN_FILE = 'OPEN_FILE',
+ TOGGLE_FILE_REVIEWED = 'TOGGLE_FILE_REVIEWED',
+ TOGGLE_ALL_INLINE_DIFFS = 'TOGGLE_ALL_INLINE_DIFFS',
+ TOGGLE_INLINE_DIFF = 'TOGGLE_INLINE_DIFF',
+ TOGGLE_HIDE_ALL_COMMENT_THREADS = 'TOGGLE_HIDE_ALL_COMMENT_THREADS',
+ OPEN_FILE_LIST = 'OPEN_FILE_LIST',
+
+ OPEN_FIRST_FILE = 'OPEN_FIRST_FILE',
+ OPEN_LAST_FILE = 'OPEN_LAST_FILE',
+
+ SEARCH = 'SEARCH',
+ SEND_REPLY = 'SEND_REPLY',
+ EMOJI_DROPDOWN = 'EMOJI_DROPDOWN',
+ TOGGLE_BLAME = 'TOGGLE_BLAME',
+}
+
+export interface ShortcutHelpItem {
+ shortcut: Shortcut;
+ text: string;
+ bindings: string[];
+}
+
+export const config = new Map<ShortcutSection, ShortcutHelpItem[]>();
+
+function describe(
+ shortcut: Shortcut,
+ section: ShortcutSection,
+ text: string,
+ binding: string,
+ ...moreBindings: string[]
+) {
+ if (!config.has(section)) {
+ config.set(section, []);
+ }
+ const shortcuts = config.get(section);
+ if (shortcuts) {
+ shortcuts.push({shortcut, text, bindings: [binding, ...moreBindings]});
+ }
+}
+
+describe(Shortcut.SEARCH, ShortcutSection.EVERYWHERE, 'Search', '/');
+describe(
+ Shortcut.OPEN_SHORTCUT_HELP_DIALOG,
+ ShortcutSection.EVERYWHERE,
+ 'Show this dialog',
+ '?'
+);
+describe(
+ Shortcut.GO_TO_USER_DASHBOARD,
+ ShortcutSection.EVERYWHERE,
+ 'Go to User Dashboard',
+ SPECIAL_SHORTCUT.GO_KEY,
+ 'i'
+);
+describe(
+ Shortcut.GO_TO_OPENED_CHANGES,
+ ShortcutSection.EVERYWHERE,
+ 'Go to Opened Changes',
+ SPECIAL_SHORTCUT.GO_KEY,
+ 'o'
+);
+describe(
+ Shortcut.GO_TO_MERGED_CHANGES,
+ ShortcutSection.EVERYWHERE,
+ 'Go to Merged Changes',
+ SPECIAL_SHORTCUT.GO_KEY,
+ 'm'
+);
+describe(
+ Shortcut.GO_TO_ABANDONED_CHANGES,
+ ShortcutSection.EVERYWHERE,
+ 'Go to Abandoned Changes',
+ SPECIAL_SHORTCUT.GO_KEY,
+ 'a'
+);
+describe(
+ Shortcut.GO_TO_WATCHED_CHANGES,
+ ShortcutSection.EVERYWHERE,
+ 'Go to Watched Changes',
+ SPECIAL_SHORTCUT.GO_KEY,
+ 'w'
+);
+
+describe(
+ Shortcut.CURSOR_NEXT_CHANGE,
+ ShortcutSection.ACTIONS,
+ 'Select next change',
+ 'j'
+);
+describe(
+ Shortcut.CURSOR_PREV_CHANGE,
+ ShortcutSection.ACTIONS,
+ 'Select previous change',
+ 'k'
+);
+describe(
+ Shortcut.OPEN_CHANGE,
+ ShortcutSection.ACTIONS,
+ 'Show selected change',
+ 'o'
+);
+describe(
+ Shortcut.NEXT_PAGE,
+ ShortcutSection.ACTIONS,
+ 'Go to next page',
+ 'n',
+ ']'
+);
+describe(
+ Shortcut.PREV_PAGE,
+ ShortcutSection.ACTIONS,
+ 'Go to previous page',
+ 'p',
+ '['
+);
+describe(
+ Shortcut.OPEN_REPLY_DIALOG,
+ ShortcutSection.ACTIONS,
+ 'Open reply dialog to publish comments and add reviewers',
+ 'a:keyup'
+);
+describe(
+ Shortcut.OPEN_DOWNLOAD_DIALOG,
+ ShortcutSection.ACTIONS,
+ 'Open download overlay',
+ 'd:keyup'
+);
+describe(
+ Shortcut.EXPAND_ALL_MESSAGES,
+ ShortcutSection.ACTIONS,
+ 'Expand all messages',
+ 'x'
+);
+describe(
+ Shortcut.COLLAPSE_ALL_MESSAGES,
+ ShortcutSection.ACTIONS,
+ 'Collapse all messages',
+ 'z'
+);
+describe(
+ Shortcut.REFRESH_CHANGE,
+ ShortcutSection.ACTIONS,
+ 'Reload the change at the latest patch',
+ 'shift+r:keyup'
+);
+describe(
+ Shortcut.TOGGLE_CHANGE_REVIEWED,
+ ShortcutSection.ACTIONS,
+ 'Mark/unmark change as reviewed',
+ 'r:keyup'
+);
+describe(
+ Shortcut.TOGGLE_FILE_REVIEWED,
+ ShortcutSection.ACTIONS,
+ 'Toggle review flag on selected file',
+ 'r:keyup'
+);
+describe(
+ Shortcut.REFRESH_CHANGE_LIST,
+ ShortcutSection.ACTIONS,
+ 'Refresh list of changes',
+ 'shift+r:keyup'
+);
+describe(
+ Shortcut.TOGGLE_CHANGE_STAR,
+ ShortcutSection.ACTIONS,
+ 'Star/unstar change',
+ 's:keydown'
+);
+describe(
+ Shortcut.OPEN_SUBMIT_DIALOG,
+ ShortcutSection.ACTIONS,
+ 'Open submit dialog',
+ 'shift+s'
+);
+describe(
+ Shortcut.TOGGLE_ATTENTION_SET,
+ ShortcutSection.ACTIONS,
+ 'Toggle attention set status',
+ 'shift+t'
+);
+describe(
+ Shortcut.EDIT_TOPIC,
+ ShortcutSection.ACTIONS,
+ 'Add a change topic',
+ 't'
+);
+describe(
+ Shortcut.DIFF_AGAINST_BASE,
+ ShortcutSection.DIFFS,
+ 'Diff against base',
+ SPECIAL_SHORTCUT.V_KEY,
+ 'down',
+ 's'
+);
+describe(
+ Shortcut.DIFF_AGAINST_LATEST,
+ ShortcutSection.DIFFS,
+ 'Diff against latest patchset',
+ SPECIAL_SHORTCUT.V_KEY,
+ 'up',
+ 'w'
+);
+describe(
+ Shortcut.DIFF_BASE_AGAINST_LEFT,
+ ShortcutSection.DIFFS,
+ 'Diff base against left',
+ SPECIAL_SHORTCUT.V_KEY,
+ 'left',
+ 'a'
+);
+describe(
+ Shortcut.DIFF_RIGHT_AGAINST_LATEST,
+ ShortcutSection.DIFFS,
+ 'Diff right against latest',
+ SPECIAL_SHORTCUT.V_KEY,
+ 'right',
+ 'd'
+);
+describe(
+ Shortcut.DIFF_BASE_AGAINST_LATEST,
+ ShortcutSection.DIFFS,
+ 'Diff base against latest',
+ SPECIAL_SHORTCUT.V_KEY,
+ 'b'
+);
+
+describe(
+ Shortcut.NEXT_LINE,
+ ShortcutSection.DIFFS,
+ 'Go to next line',
+ 'j',
+ 'down'
+);
+describe(
+ Shortcut.PREV_LINE,
+ ShortcutSection.DIFFS,
+ 'Go to previous line',
+ 'k',
+ 'up'
+);
+describe(
+ Shortcut.VISIBLE_LINE,
+ ShortcutSection.DIFFS,
+ 'Move cursor to currently visible code',
+ '.'
+);
+describe(
+ Shortcut.NEXT_CHUNK,
+ ShortcutSection.DIFFS,
+ 'Go to next diff chunk',
+ 'n'
+);
+describe(
+ Shortcut.PREV_CHUNK,
+ ShortcutSection.DIFFS,
+ 'Go to previous diff chunk',
+ 'p'
+);
+describe(
+ Shortcut.TOGGLE_ALL_DIFF_CONTEXT,
+ ShortcutSection.DIFFS,
+ 'Toggle all diff context',
+ 'shift+x'
+);
+describe(
+ Shortcut.NEXT_COMMENT_THREAD,
+ ShortcutSection.DIFFS,
+ 'Go to next comment thread',
+ 'shift+n'
+);
+describe(
+ Shortcut.PREV_COMMENT_THREAD,
+ ShortcutSection.DIFFS,
+ 'Go to previous comment thread',
+ 'shift+p'
+);
+describe(
+ Shortcut.EXPAND_ALL_COMMENT_THREADS,
+ ShortcutSection.DIFFS,
+ 'Expand all comment threads',
+ SPECIAL_SHORTCUT.DOC_ONLY,
+ 'e'
+);
+describe(
+ Shortcut.COLLAPSE_ALL_COMMENT_THREADS,
+ ShortcutSection.DIFFS,
+ 'Collapse all comment threads',
+ SPECIAL_SHORTCUT.DOC_ONLY,
+ 'shift+e'
+);
+describe(
+ Shortcut.TOGGLE_HIDE_ALL_COMMENT_THREADS,
+ ShortcutSection.DIFFS,
+ 'Hide/Display all comment threads',
+ 'h'
+);
+describe(
+ Shortcut.LEFT_PANE,
+ ShortcutSection.DIFFS,
+ 'Select left pane',
+ 'shift+left'
+);
+describe(
+ Shortcut.RIGHT_PANE,
+ ShortcutSection.DIFFS,
+ 'Select right pane',
+ 'shift+right'
+);
+describe(
+ Shortcut.TOGGLE_LEFT_PANE,
+ ShortcutSection.DIFFS,
+ 'Hide/show left diff',
+ 'shift+a'
+);
+describe(Shortcut.NEW_COMMENT, ShortcutSection.DIFFS, 'Draft new comment', 'c');
+describe(
+ Shortcut.SAVE_COMMENT,
+ ShortcutSection.DIFFS,
+ 'Save comment',
+ 'ctrl+enter',
+ 'meta+enter',
+ 'ctrl+s',
+ 'meta+s'
+);
+describe(
+ Shortcut.OPEN_DIFF_PREFS,
+ ShortcutSection.DIFFS,
+ 'Show diff preferences',
+ ','
+);
+describe(
+ Shortcut.TOGGLE_DIFF_REVIEWED,
+ ShortcutSection.DIFFS,
+ 'Mark/unmark file as reviewed',
+ 'r:keyup'
+);
+describe(
+ Shortcut.TOGGLE_DIFF_MODE,
+ ShortcutSection.DIFFS,
+ 'Toggle unified/side-by-side diff',
+ 'm:keyup'
+);
+describe(
+ Shortcut.NEXT_UNREVIEWED_FILE,
+ ShortcutSection.DIFFS,
+ 'Mark file as reviewed and go to next unreviewed file',
+ 'shift+m'
+);
+describe(
+ Shortcut.TOGGLE_BLAME,
+ ShortcutSection.DIFFS,
+ 'Toggle blame',
+ 'b:keyup'
+);
+describe(Shortcut.OPEN_FILE_LIST, ShortcutSection.DIFFS, 'Open file list', 'f');
+describe(
+ Shortcut.NEXT_FILE,
+ ShortcutSection.NAVIGATION,
+ 'Go to next file',
+ ']'
+);
+describe(
+ Shortcut.PREV_FILE,
+ ShortcutSection.NAVIGATION,
+ 'Go to previous file',
+ '['
+);
+describe(
+ Shortcut.NEXT_FILE_WITH_COMMENTS,
+ ShortcutSection.NAVIGATION,
+ 'Go to next file that has comments',
+ 'shift+j'
+);
+describe(
+ Shortcut.PREV_FILE_WITH_COMMENTS,
+ ShortcutSection.NAVIGATION,
+ 'Go to previous file that has comments',
+ 'shift+k'
+);
+describe(
+ Shortcut.OPEN_FIRST_FILE,
+ ShortcutSection.NAVIGATION,
+ 'Go to first file',
+ ']'
+);
+describe(
+ Shortcut.OPEN_LAST_FILE,
+ ShortcutSection.NAVIGATION,
+ 'Go to last file',
+ '['
+);
+describe(
+ Shortcut.UP_TO_DASHBOARD,
+ ShortcutSection.NAVIGATION,
+ 'Up to dashboard',
+ 'u'
+);
+describe(
+ Shortcut.UP_TO_CHANGE,
+ ShortcutSection.NAVIGATION,
+ 'Up to change',
+ 'u'
+);
+
+describe(
+ Shortcut.CURSOR_NEXT_FILE,
+ ShortcutSection.FILE_LIST,
+ 'Select next file',
+ 'j',
+ 'down'
+);
+describe(
+ Shortcut.CURSOR_PREV_FILE,
+ ShortcutSection.FILE_LIST,
+ 'Select previous file',
+ 'k',
+ 'up'
+);
+describe(
+ Shortcut.OPEN_FILE,
+ ShortcutSection.FILE_LIST,
+ 'Go to selected file',
+ 'o',
+ 'enter'
+);
+describe(
+ Shortcut.TOGGLE_ALL_INLINE_DIFFS,
+ ShortcutSection.FILE_LIST,
+ 'Show/hide all inline diffs',
+ 'shift+i'
+);
+describe(
+ Shortcut.TOGGLE_INLINE_DIFF,
+ ShortcutSection.FILE_LIST,
+ 'Show/hide selected inline diff',
+ 'i'
+);
+
+describe(
+ Shortcut.SEND_REPLY,
+ ShortcutSection.REPLY_DIALOG,
+ 'Send reply',
+ SPECIAL_SHORTCUT.DOC_ONLY,
+ 'ctrl+enter',
+ 'meta+enter'
+);
+describe(
+ Shortcut.EMOJI_DROPDOWN,
+ ShortcutSection.REPLY_DIALOG,
+ 'Emoji dropdown',
+ SPECIAL_SHORTCUT.DOC_ONLY,
+ ':'
+);
diff --git a/polygerrit-ui/app/services/shortcuts/shortcuts-service.ts b/polygerrit-ui/app/services/shortcuts/shortcuts-service.ts
new file mode 100644
index 0000000..d0e2d49
--- /dev/null
+++ b/polygerrit-ui/app/services/shortcuts/shortcuts-service.ts
@@ -0,0 +1,317 @@
+/**
+ * @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 {
+ config,
+ Shortcut,
+ ShortcutHelpItem,
+ ShortcutSection,
+ SPECIAL_SHORTCUT,
+} from './shortcuts-config';
+import {disableShortcuts$} from '../user/user-model';
+import {IronKeyboardEvent, isIronKeyboardEvent} from '../../types/events';
+import {isElementTarget} from '../../utils/dom-util';
+import {ReportingService} from '../gr-reporting/gr-reporting';
+
+export type SectionView = Array<{binding: string[][]; text: string}>;
+
+/**
+ * The interface for listener for shortcut events.
+ */
+export type ShortcutListener = (
+ viewMap?: Map<ShortcutSection, SectionView>
+) => void;
+
+const COMBO_KEYS = ['g', 'v'];
+
+/**
+ * Shortcuts service, holds all hosts, bindings and listeners.
+ */
+export class ShortcutsService {
+ /**
+ * Keeps track of the components that are currently active such that we can
+ * show a shortcut help dialog that only shows the shortcuts that are
+ * currently relevant.
+ */
+ private readonly activeHosts = new Map<unknown, Map<string, string>>();
+
+ /** Static map built in the constructor by iterating over the config. */
+ private readonly bindings = new Map<Shortcut, string[]>();
+
+ private readonly listeners = new Set<ShortcutListener>();
+
+ /**
+ * Maps keys (e.g. 'g') to the timestamp when they have last been pressed.
+ * This enabled key combinations like 'g+o' where we can check whether 'g' was
+ * pressed recently when 'o' is processed. Keys of this map must be items of
+ * COMBO_KEYS. Values are Date timestamps in milliseconds.
+ */
+ private readonly keyLastPressed = new Map<string, number>();
+
+ /** Keeps track of the corresponding user preference. */
+ private shortcutsDisabled = false;
+
+ constructor(readonly reporting?: ReportingService) {
+ for (const section of config.keys()) {
+ const items = config.get(section) ?? [];
+ for (const item of items) {
+ this.bindings.set(item.shortcut, item.bindings);
+ }
+ }
+ disableShortcuts$.subscribe(x => (this.shortcutsDisabled = x));
+ document.addEventListener('keydown', (e: KeyboardEvent) => {
+ if (!COMBO_KEYS.includes(e.key)) return;
+ if (this.shouldSuppress(e)) return;
+ this.keyLastPressed.set(e.key, Date.now());
+ });
+ }
+
+ public _testOnly_isEmpty() {
+ return this.activeHosts.size === 0 && this.listeners.size === 0;
+ }
+
+ shouldSuppress(event: IronKeyboardEvent | KeyboardEvent) {
+ if (this.shortcutsDisabled) return true;
+ const e = isIronKeyboardEvent(event) ? event.detail.keyboardEvent : event;
+
+ // Note that when you listen on document, then `e.currentTarget` will be the
+ // document and `e.target` will be `<gr-app>` due to shadow dom, but by
+ // using the composedPath() you can actually find the true origin of the
+ // event.
+ const rootTarget = e.composedPath()[0];
+ if (!isElementTarget(rootTarget)) return false;
+ const tagName = rootTarget.tagName;
+ const type = rootTarget.getAttribute('type');
+
+ if (
+ // Suppress shortcuts on <input> and <textarea>, but not on
+ // checkboxes, because we want to enable workflows like 'click
+ // mark-reviewed and then press ] to go to the next file'.
+ (tagName === 'INPUT' && type !== 'checkbox') ||
+ tagName === 'TEXTAREA' ||
+ // Suppress shortcuts if the key is 'enter'
+ // and target is an anchor or button or paper-tab.
+ (e.keyCode === 13 &&
+ (tagName === 'A' || tagName === 'BUTTON' || tagName === 'PAPER-TAB'))
+ ) {
+ return true;
+ }
+ const path: EventTarget[] = e.composedPath() ?? [];
+ for (const el of path) {
+ if (!isElementTarget(el)) continue;
+ if (el.tagName === 'GR-OVERLAY') return true;
+ }
+ // eg: {key: "k:keydown", ..., from: "gr-diff-view"}
+ let key = `${e.key}:${e.type}`;
+ // TODO(brohlfs): Re-enable reporting of g- and v-keys.
+ // if (this._inGoKeyMode()) key = 'g+' + key;
+ // if (this.inVKeyMode()) key = 'v+' + key;
+ if (e.shiftKey) key = 'shift+' + key;
+ if (e.ctrlKey) key = 'ctrl+' + key;
+ if (e.metaKey) key = 'meta+' + key;
+ if (e.altKey) key = 'alt+' + key;
+ let from = 'unknown';
+ if (isElementTarget(e.currentTarget)) {
+ from = e.currentTarget.tagName;
+ }
+ this.reporting?.reportInteraction('shortcut-triggered', {key, from});
+ return false;
+ }
+
+ createTitle(shortcutName: Shortcut, section: ShortcutSection) {
+ const desc = this.getDescription(section, shortcutName);
+ const shortcut = this.getShortcut(shortcutName);
+ return desc && shortcut ? `${desc} (shortcut: ${shortcut})` : '';
+ }
+
+ getBindingsForShortcut(shortcut: Shortcut) {
+ return this.bindings.get(shortcut);
+ }
+
+ attachHost(host: unknown, shortcuts: Map<string, string>) {
+ this.activeHosts.set(host, shortcuts);
+ this.notifyListeners();
+ }
+
+ detachHost(host: unknown) {
+ if (!this.activeHosts.delete(host)) return false;
+ this.notifyListeners();
+ return true;
+ }
+
+ addListener(listener: ShortcutListener) {
+ this.listeners.add(listener);
+ listener(this.directoryView());
+ }
+
+ removeListener(listener: ShortcutListener) {
+ return this.listeners.delete(listener);
+ }
+
+ getDescription(section: ShortcutSection, shortcutName: Shortcut) {
+ const bindings = config.get(section);
+ if (!bindings) return '';
+ const binding = bindings.find(binding => binding.shortcut === shortcutName);
+ return binding?.text ?? '';
+ }
+
+ getShortcut(shortcutName: Shortcut) {
+ const bindings = this.bindings.get(shortcutName);
+ if (!bindings) return '';
+ return bindings
+ .map(binding => this.describeBinding(binding).join('+'))
+ .join(',');
+ }
+
+ activeShortcutsBySection() {
+ const activeShortcuts = new Set<string>();
+ this.activeHosts.forEach(shortcuts => {
+ shortcuts.forEach((_, shortcut) => activeShortcuts.add(shortcut));
+ });
+
+ const activeShortcutsBySection = new Map<
+ ShortcutSection,
+ ShortcutHelpItem[]
+ >();
+ config.forEach((shortcutList, section) => {
+ shortcutList.forEach(shortcutHelp => {
+ if (activeShortcuts.has(shortcutHelp.shortcut)) {
+ if (!activeShortcutsBySection.has(section)) {
+ activeShortcutsBySection.set(section, []);
+ }
+ // From previous condition, the `get(section)`
+ // should always return a valid result
+ activeShortcutsBySection.get(section)!.push(shortcutHelp);
+ }
+ });
+ });
+ return activeShortcutsBySection;
+ }
+
+ directoryView() {
+ const view = new Map<ShortcutSection, SectionView>();
+ this.activeShortcutsBySection().forEach((shortcutHelps, section) => {
+ const sectionView: SectionView = [];
+ shortcutHelps.forEach(shortcutHelp => {
+ const bindingDesc = this.describeBindings(shortcutHelp.shortcut);
+ if (!bindingDesc) {
+ return;
+ }
+ this.distributeBindingDesc(bindingDesc).forEach(bindingDesc => {
+ sectionView.push({
+ binding: bindingDesc,
+ text: shortcutHelp.text,
+ });
+ });
+ });
+ view.set(section, sectionView);
+ });
+ return view;
+ }
+
+ distributeBindingDesc(bindingDesc: string[][]): string[][][] {
+ if (
+ bindingDesc.length === 1 ||
+ this.comboSetDisplayWidth(bindingDesc) < 21
+ ) {
+ return [bindingDesc];
+ }
+ // Find the largest prefix of bindings that is under the
+ // size threshold.
+ const head = [bindingDesc[0]];
+ for (let i = 1; i < bindingDesc.length; i++) {
+ head.push(bindingDesc[i]);
+ if (this.comboSetDisplayWidth(head) >= 21) {
+ head.pop();
+ return [head].concat(this.distributeBindingDesc(bindingDesc.slice(i)));
+ }
+ }
+ return [];
+ }
+
+ comboSetDisplayWidth(bindingDesc: string[][]) {
+ const bindingSizer = (binding: string[]) =>
+ binding.reduce((acc, key) => acc + key.length, 0);
+ // Width is the sum of strings + (n-1) * 2 to account for the word
+ // "or" joining them.
+ return (
+ bindingDesc.reduce((acc, binding) => acc + bindingSizer(binding), 0) +
+ 2 * (bindingDesc.length - 1)
+ );
+ }
+
+ describeBindings(shortcut: Shortcut): string[][] | null {
+ const bindings = this.bindings.get(shortcut);
+ if (!bindings) {
+ return null;
+ }
+ if (bindings[0] === SPECIAL_SHORTCUT.GO_KEY) {
+ return bindings
+ .slice(1)
+ .map(binding => this._describeKey(binding))
+ .map(binding => ['g'].concat(binding));
+ }
+ if (bindings[0] === SPECIAL_SHORTCUT.V_KEY) {
+ return bindings
+ .slice(1)
+ .map(binding => this._describeKey(binding))
+ .map(binding => ['v'].concat(binding));
+ }
+
+ return bindings
+ .filter(binding => binding !== SPECIAL_SHORTCUT.DOC_ONLY)
+ .map(binding => this.describeBinding(binding));
+ }
+
+ _describeKey(key: string) {
+ switch (key) {
+ case 'shift':
+ return 'Shift';
+ case 'meta':
+ return 'Meta';
+ case 'ctrl':
+ return 'Ctrl';
+ case 'enter':
+ return 'Enter';
+ case 'up':
+ return '\u2191'; // ↑
+ case 'down':
+ return '\u2193'; // ↓
+ case 'left':
+ return '\u2190'; // ←
+ case 'right':
+ return '\u2192'; // →
+ default:
+ return key;
+ }
+ }
+
+ describeBinding(binding: string) {
+ // single key bindings
+ if (binding.length === 1) {
+ return [binding];
+ }
+ return binding
+ .split(':')[0]
+ .split('+')
+ .map(part => this._describeKey(part));
+ }
+
+ notifyListeners() {
+ const view = this.directoryView();
+ this.listeners.forEach(listener => listener(view));
+ }
+}
diff --git a/polygerrit-ui/app/services/shortcuts/shortcuts-service_test.ts b/polygerrit-ui/app/services/shortcuts/shortcuts-service_test.ts
new file mode 100644
index 0000000..0998a4c
--- /dev/null
+++ b/polygerrit-ui/app/services/shortcuts/shortcuts-service_test.ts
@@ -0,0 +1,293 @@
+/**
+ * @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';
+import {ShortcutsService} from '../../services/shortcuts/shortcuts-service';
+import {Shortcut, ShortcutSection} from './shortcuts-config';
+import * as MockInteractions from '@polymer/iron-test-helpers/mock-interactions';
+
+async function keyEventOn(
+ el: HTMLElement,
+ callback: (e: KeyboardEvent) => void,
+ keyCode = 75,
+ key = 'k'
+): Promise<KeyboardEvent> {
+ let resolve: (e: KeyboardEvent) => void;
+ const promise = new Promise<KeyboardEvent>(r => (resolve = r));
+ el.addEventListener('keydown', (e: KeyboardEvent) => {
+ callback(e);
+ resolve(e);
+ });
+ MockInteractions.keyDownOn(el, keyCode, null, key);
+ return await promise;
+}
+
+suite('shortcuts-service tests', () => {
+ let service: ShortcutsService;
+
+ setup(() => {
+ service = new ShortcutsService();
+ });
+
+ suite('shouldSuppress', () => {
+ test('do not suppress shortcut event from <div>', async () => {
+ await keyEventOn(document.createElement('div'), e => {
+ assert.isFalse(service.shouldSuppress(e));
+ });
+ });
+
+ test('suppress shortcut event from <input>', async () => {
+ await keyEventOn(document.createElement('input'), e => {
+ assert.isTrue(service.shouldSuppress(e));
+ });
+ });
+
+ test('suppress shortcut event from <textarea>', async () => {
+ await keyEventOn(document.createElement('textarea'), e => {
+ assert.isTrue(service.shouldSuppress(e));
+ });
+ });
+
+ test('do not suppress shortcut event from checkbox <input>', async () => {
+ const inputEl = document.createElement('input');
+ inputEl.setAttribute('type', 'checkbox');
+ await keyEventOn(inputEl, e => {
+ assert.isFalse(service.shouldSuppress(e));
+ });
+ });
+
+ test('suppress shortcut event from children of <gr-overlay>', async () => {
+ const overlay = document.createElement('gr-overlay');
+ const div = document.createElement('div');
+ overlay.appendChild(div);
+ await keyEventOn(div, e => {
+ assert.isTrue(service.shouldSuppress(e));
+ });
+ });
+
+ test('suppress "enter" shortcut event from <a>', async () => {
+ await keyEventOn(document.createElement('a'), e => {
+ assert.isFalse(service.shouldSuppress(e));
+ });
+ await keyEventOn(
+ document.createElement('a'),
+ e => assert.isTrue(service.shouldSuppress(e)),
+ 13,
+ 'enter'
+ );
+ });
+ });
+
+ test('getShortcut', () => {
+ const NEXT_FILE = Shortcut.NEXT_FILE;
+ assert.equal(service.getShortcut(NEXT_FILE), ']');
+ });
+
+ test('getShortcut with modifiers', () => {
+ const NEXT_FILE = Shortcut.TOGGLE_LEFT_PANE;
+ assert.equal(service.getShortcut(NEXT_FILE), 'Shift+a');
+ });
+
+ suite('binding descriptions', () => {
+ function mapToObject<K, V>(m: Map<K, V>) {
+ const o: any = {};
+ m.forEach((v: V, k: K) => (o[k] = v));
+ return o;
+ }
+
+ test('single combo description', () => {
+ assert.deepEqual(service.describeBinding('a'), ['a']);
+ assert.deepEqual(service.describeBinding('a:keyup'), ['a']);
+ assert.deepEqual(service.describeBinding('ctrl+a'), ['Ctrl', 'a']);
+ assert.deepEqual(service.describeBinding('ctrl+shift+up:keyup'), [
+ 'Ctrl',
+ 'Shift',
+ '↑',
+ ]);
+ });
+
+ test('combo set description', () => {
+ assert.deepEqual(
+ service.describeBindings(Shortcut.GO_TO_OPENED_CHANGES),
+ [['g', 'o']]
+ );
+ assert.deepEqual(service.describeBindings(Shortcut.SAVE_COMMENT), [
+ ['Ctrl', 'Enter'],
+ ['Meta', 'Enter'],
+ ['Ctrl', 's'],
+ ['Meta', 's'],
+ ]);
+ assert.deepEqual(service.describeBindings(Shortcut.PREV_FILE), [['[']]);
+ });
+
+ test('combo set description width', () => {
+ assert.strictEqual(service.comboSetDisplayWidth([['u']]), 1);
+ assert.strictEqual(service.comboSetDisplayWidth([['g', 'o']]), 2);
+ assert.strictEqual(service.comboSetDisplayWidth([['Shift', 'r']]), 6);
+ assert.strictEqual(service.comboSetDisplayWidth([['x'], ['y']]), 4);
+ assert.strictEqual(
+ service.comboSetDisplayWidth([['x'], ['y'], ['Shift', 'z']]),
+ 12
+ );
+ });
+
+ test('distribute shortcut help', () => {
+ assert.deepEqual(service.distributeBindingDesc([['o']]), [[['o']]]);
+ assert.deepEqual(service.distributeBindingDesc([['g', 'o']]), [
+ [['g', 'o']],
+ ]);
+ assert.deepEqual(
+ service.distributeBindingDesc([['ctrl', 'shift', 'meta', 'enter']]),
+ [[['ctrl', 'shift', 'meta', 'enter']]]
+ );
+ assert.deepEqual(
+ service.distributeBindingDesc([
+ ['ctrl', 'shift', 'meta', 'enter'],
+ ['o'],
+ ]),
+ [[['ctrl', 'shift', 'meta', 'enter']], [['o']]]
+ );
+ assert.deepEqual(
+ service.distributeBindingDesc([
+ ['ctrl', 'enter'],
+ ['meta', 'enter'],
+ ['ctrl', 's'],
+ ['meta', 's'],
+ ]),
+ [
+ [
+ ['ctrl', 'enter'],
+ ['meta', 'enter'],
+ ],
+ [
+ ['ctrl', 's'],
+ ['meta', 's'],
+ ],
+ ]
+ );
+ });
+
+ test('active shortcuts by section', () => {
+ assert.deepEqual(mapToObject(service.activeShortcutsBySection()), {});
+
+ service.attachHost({}, new Map([[Shortcut.NEXT_FILE, 'null']]));
+ assert.deepEqual(mapToObject(service.activeShortcutsBySection()), {
+ [ShortcutSection.NAVIGATION]: [
+ {
+ shortcut: Shortcut.NEXT_FILE,
+ text: 'Go to next file',
+ bindings: [']'],
+ },
+ ],
+ });
+
+ service.attachHost({}, new Map([[Shortcut.NEXT_LINE, 'null']]));
+ assert.deepEqual(mapToObject(service.activeShortcutsBySection()), {
+ [ShortcutSection.DIFFS]: [
+ {
+ shortcut: Shortcut.NEXT_LINE,
+ text: 'Go to next line',
+ bindings: ['j', 'down'],
+ },
+ ],
+ [ShortcutSection.NAVIGATION]: [
+ {
+ shortcut: Shortcut.NEXT_FILE,
+ text: 'Go to next file',
+ bindings: [']'],
+ },
+ ],
+ });
+
+ service.attachHost(
+ {},
+ new Map([
+ [Shortcut.SEARCH, 'null'],
+ [Shortcut.GO_TO_OPENED_CHANGES, 'null'],
+ ])
+ );
+ assert.deepEqual(mapToObject(service.activeShortcutsBySection()), {
+ [ShortcutSection.DIFFS]: [
+ {
+ shortcut: Shortcut.NEXT_LINE,
+ text: 'Go to next line',
+ bindings: ['j', 'down'],
+ },
+ ],
+ [ShortcutSection.EVERYWHERE]: [
+ {
+ shortcut: Shortcut.SEARCH,
+ text: 'Search',
+ bindings: ['/'],
+ },
+ {
+ shortcut: Shortcut.GO_TO_OPENED_CHANGES,
+ text: 'Go to Opened Changes',
+ bindings: ['GO_KEY', 'o'],
+ },
+ ],
+ [ShortcutSection.NAVIGATION]: [
+ {
+ shortcut: Shortcut.NEXT_FILE,
+ text: 'Go to next file',
+ bindings: [']'],
+ },
+ ],
+ });
+ });
+
+ test('directory view', () => {
+ assert.deepEqual(mapToObject(service.directoryView()), {});
+
+ service.attachHost(
+ {},
+ new Map([
+ [Shortcut.GO_TO_OPENED_CHANGES, 'null'],
+ [Shortcut.NEXT_FILE, 'null'],
+ [Shortcut.NEXT_LINE, 'null'],
+ [Shortcut.SAVE_COMMENT, 'null'],
+ [Shortcut.SEARCH, 'null'],
+ ])
+ );
+ assert.deepEqual(mapToObject(service.directoryView()), {
+ [ShortcutSection.DIFFS]: [
+ {binding: [['j'], ['↓']], text: 'Go to next line'},
+ {
+ binding: [
+ ['Ctrl', 'Enter'],
+ ['Meta', 'Enter'],
+ ],
+ text: 'Save comment',
+ },
+ {
+ binding: [
+ ['Ctrl', 's'],
+ ['Meta', 's'],
+ ],
+ text: 'Save comment',
+ },
+ ],
+ [ShortcutSection.EVERYWHERE]: [
+ {binding: [['/']], text: 'Search'},
+ {binding: [['g', 'o']], text: 'Go to Opened Changes'},
+ ],
+ [ShortcutSection.NAVIGATION]: [
+ {binding: [[']']], text: 'Go to next file'},
+ ],
+ });
+ });
+ });
+});
diff --git a/polygerrit-ui/app/services/user/user-model.ts b/polygerrit-ui/app/services/user/user-model.ts
index 4115a71..72ce3e1 100644
--- a/polygerrit-ui/app/services/user/user-model.ts
+++ b/polygerrit-ui/app/services/user/user-model.ts
@@ -60,3 +60,8 @@
map(preferences => preferences?.my ?? []),
distinctUntilChanged()
);
+
+export const disableShortcuts$ = preferences$.pipe(
+ map(preferences => preferences?.disable_keyboard_shortcuts ?? false),
+ distinctUntilChanged()
+);
diff --git a/polygerrit-ui/app/styles/gr-a11y-styles.ts b/polygerrit-ui/app/styles/gr-a11y-styles.ts
new file mode 100644
index 0000000..a1fa62b
--- /dev/null
+++ b/polygerrit-ui/app/styles/gr-a11y-styles.ts
@@ -0,0 +1,42 @@
+/**
+ * @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} from 'lit';
+
+export const a11yStyles = css`
+ .assistive-tech-only {
+ user-select: none;
+ clip: rect(1px, 1px, 1px, 1px);
+ height: 1px;
+ margin: 0;
+ overflow: hidden;
+ padding: 0;
+ position: absolute;
+ white-space: nowrap;
+ width: 1px;
+ z-index: -1000;
+ }
+`;
+
+const $_documentContainer = document.createElement('template');
+$_documentContainer.innerHTML = `<dom-module id="gr-a11y-styles">
+ <template>
+ <style>
+ ${a11yStyles.cssText}
+ </style>
+ </template>
+</dom-module>`;
+document.head.appendChild($_documentContainer.content);
diff --git a/polygerrit-ui/app/styles/gr-hovercard-styles.ts b/polygerrit-ui/app/styles/gr-hovercard-styles.ts
new file mode 100644
index 0000000..f214a9c
--- /dev/null
+++ b/polygerrit-ui/app/styles/gr-hovercard-styles.ts
@@ -0,0 +1,51 @@
+/**
+ * @license
+ * Copyright (C) 2017 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} from 'lit';
+
+export const hovercardStyles = css`
+ :host {
+ position: absolute;
+ display: none;
+ z-index: 200;
+ max-width: 600px;
+ outline: none;
+ }
+ :host(.hovered) {
+ display: block;
+ }
+ :host(.hide) {
+ visibility: hidden;
+ }
+ /* You have to use a <div class="container"> in your hovercard in order
+ to pick up this consistent styling. */
+ #container {
+ background: var(--dialog-background-color);
+ border: 1px solid var(--border-color);
+ border-radius: var(--border-radius);
+ box-shadow: var(--elevation-level-5);
+ }
+`;
+
+const $_documentContainer = document.createElement('template');
+$_documentContainer.innerHTML = `<dom-module id="gr-hovercard-styles">
+ <template>
+ <style>
+ ${hovercardStyles.cssText}
+ </style>
+ </template>
+</dom-module>`;
+document.head.appendChild($_documentContainer.content);
diff --git a/polygerrit-ui/app/styles/gr-spinner-styles.ts b/polygerrit-ui/app/styles/gr-spinner-styles.ts
index 0f8341f..6015be4 100644
--- a/polygerrit-ui/app/styles/gr-spinner-styles.ts
+++ b/polygerrit-ui/app/styles/gr-spinner-styles.ts
@@ -16,13 +16,6 @@
*/
import {css} from 'lit';
-// Mark the file as a module. Otherwise typescript assumes this is a script
-// and $_documentContainer is a global variable.
-// See: https://www.typescriptlang.org/docs/handbook/modules.html
-export {};
-
-const $_documentContainer = document.createElement('template');
-
export const spinnerStyles = css`
.loadingSpin {
border: 2px solid var(--disabled-button-background-color);
@@ -43,6 +36,7 @@
}
`;
+const $_documentContainer = document.createElement('template');
$_documentContainer.innerHTML = `<dom-module id="gr-spinner-styles">
<template>
<style>
diff --git a/polygerrit-ui/app/styles/gr-voting-styles.ts b/polygerrit-ui/app/styles/gr-voting-styles.ts
index 12d0784..a623d99 100644
--- a/polygerrit-ui/app/styles/gr-voting-styles.ts
+++ b/polygerrit-ui/app/styles/gr-voting-styles.ts
@@ -18,24 +18,26 @@
// Mark the file as a module. Otherwise typescript assumes this is a script
// and $_documentContainer is a global variable.
// See: https://www.typescriptlang.org/docs/handbook/modules.html
-export {};
+import {css} from 'lit';
+
+export const votingStyles = css`
+ .voteChip {
+ border: 1px solid var(--border-color);
+ /* max rounded */
+ border-radius: 1em;
+ box-shadow: none;
+ box-sizing: border-box;
+ min-width: 3em;
+ color: var(--vote-text-color);
+ }
+`;
const $_documentContainer = document.createElement('template');
-
$_documentContainer.innerHTML = `<dom-module id="gr-voting-styles">
<template>
<style>
- .voteChip {
- border: 1px solid var(--border-color);
- /* max rounded */
- border-radius: 1em;
- box-shadow: none;
- box-sizing: border-box;
- min-width: 3em;
- color: var(--vote-text-color);
- }
+ ${votingStyles.cssText}
</style>
</template>
</dom-module>`;
-
document.head.appendChild($_documentContainer.content);
diff --git a/polygerrit-ui/app/styles/shared-styles.ts b/polygerrit-ui/app/styles/shared-styles.ts
index b58d5b9..98f6eb2 100644
--- a/polygerrit-ui/app/styles/shared-styles.ts
+++ b/polygerrit-ui/app/styles/shared-styles.ts
@@ -224,19 +224,6 @@
--iron-autogrow-textarea_-_white-space: pre-wrap;
}
- .assistive-tech-only {
- user-select: none;
- clip: rect(1px, 1px, 1px, 1px);
- height: 1px;
- margin: 0;
- overflow: hidden;
- padding: 0;
- position: absolute;
- white-space: nowrap;
- width: 1px;
- z-index: -1000;
- }
-
/**
* TODO: Remove these rules and change (plugin) users to rely on
* gr-spinner-styles directly.
diff --git a/polygerrit-ui/app/styles/themes/app-theme.ts b/polygerrit-ui/app/styles/themes/app-theme.ts
index f1711ac..8b00a79 100644
--- a/polygerrit-ui/app/styles/themes/app-theme.ts
+++ b/polygerrit-ui/app/styles/themes/app-theme.ts
@@ -185,6 +185,7 @@
--vote-text-color: black;
--status-text-color: white;
--tooltip-text-color: white;
+ --tooltip-button-text-color: var(--gerrit-blue-dark);
--negative-red-text-color: var(--red-600);
--positive-green-text-color: var(--green-700);
--indirect-ancestor-text-color: var(--green-700);
@@ -284,7 +285,7 @@
--font-weight-bold: 500;
--font-weight-h1: 400;
--font-weight-h2: 400;
- --font-weight-h3: 400;
+ --font-weight-h3: var(--font-weight-bold, 500);
--context-control-button-font: var(--font-weight-normal) var(--font-size-normal) var(--font-family);
--code-hint-font-weight: 500;
--image-diff-button-font: var(--font-weight-normal) var(--font-size-normal) var(--font-family);
diff --git a/polygerrit-ui/app/styles/themes/dark-theme.ts b/polygerrit-ui/app/styles/themes/dark-theme.ts
index a5e725d..a24a666 100644
--- a/polygerrit-ui/app/styles/themes/dark-theme.ts
+++ b/polygerrit-ui/app/styles/themes/dark-theme.ts
@@ -94,7 +94,8 @@
--reviewed-text-color: var(--gray-300);
--vote-text-color: black;
--status-text-color: black;
- --tooltip-text-color: var(--gray-200);
+ --tooltip-text-color: var(--gray-900);
+ --tooltip-button-text-color: var(--gerrit-blue-light);
--negative-red-text-color: var(--red-200);
--positive-green-text-color: var(--green-200);
--indirect-ancestor-text-color: var(--green-200);
@@ -115,7 +116,7 @@
--hover-background-color: rgba(161, 194, 250, 0.2);
--disabled-button-background-color: #484a4d;
--selection-background-color: rgba(161, 194, 250, 0.1);
- --tooltip-background-color: var(--gray-800);
+ --tooltip-background-color: var(--gray-200);
/* comment background colors */
--comment-background-color: #3c3f43;
diff --git a/polygerrit-ui/app/test/common-test-setup.ts b/polygerrit-ui/app/test/common-test-setup.ts
index 550d3df..949c268 100644
--- a/polygerrit-ui/app/test/common-test-setup.ts
+++ b/polygerrit-ui/app/test/common-test-setup.ts
@@ -30,9 +30,7 @@
registerTestCleanup,
addIronOverlayBackdropStyleEl,
removeIronOverlayBackdropStyleEl,
- TestKeyboardShortcutBinder,
} from './test-utils';
-import {_testOnly_getShortcutManagerInstance} from '../mixins/keyboard-shortcut-mixin/keyboard-shortcut-mixin';
import {safeTypesBridge} from '../utils/safe-types-util';
import {_testOnly_initGerritPluginApi} from '../elements/shared/gr-js-api-interface/gr-gerrit';
import {initGlobalVariables} from '../elements/gr-app-global-var-init';
@@ -45,6 +43,7 @@
import {cleanUpStorage} from '../services/storage/gr-storage_mock';
import {updatePreferences} from '../services/user/user-model';
import {createDefaultPreferences} from '../constants/constants';
+import {appContext} from '../services/app-context';
declare global {
interface Window {
@@ -101,14 +100,13 @@
// If the following asserts fails - then window.stub is
// overwritten by some other code.
assert.equal(getCleanupsCount(), 0);
+ _testOnlyInitAppContext();
// The following calls is nessecary to avoid influence of previously executed
// tests.
- TestKeyboardShortcutBinder.push();
- _testOnlyInitAppContext();
initGlobalVariables();
_testOnly_initGerritPluginApi();
- const mgr = _testOnly_getShortcutManagerInstance();
- assert.isTrue(mgr._testOnly_isEmpty());
+ const shortcuts = appContext.shortcutsService;
+ assert.isTrue(shortcuts._testOnly_isEmpty());
const selection = document.getSelection();
if (selection) {
selection.removeAllRanges();
@@ -197,7 +195,6 @@
teardown(() => {
sinon.restore();
cleanupTestUtils();
- TestKeyboardShortcutBinder.pop();
checkGlobalSpace();
removeIronOverlayBackdropStyleEl();
cancelAllTasks();
diff --git a/polygerrit-ui/app/test/test-utils.ts b/polygerrit-ui/app/test/test-utils.ts
index a60c1d1..1cde372 100644
--- a/polygerrit-ui/app/test/test-utils.ts
+++ b/polygerrit-ui/app/test/test-utils.ts
@@ -17,10 +17,6 @@
import '../types/globals';
import {_testOnly_resetPluginLoader} from '../elements/shared/gr-js-api-interface/gr-plugin-loader';
import {_testOnly_resetEndpoints} from '../elements/shared/gr-js-api-interface/gr-plugin-endpoints';
-import {
- _testOnly_getShortcutManagerInstance,
- Shortcut,
-} from '../mixins/keyboard-shortcut-mixin/keyboard-shortcut-mixin';
import {appContext} from '../services/app-context';
import {RestApiService} from '../services/gr-rest-api/gr-rest-api';
import {SinonSpy} from 'sinon';
@@ -53,44 +49,6 @@
return getComputedStyle(el).getPropertyValue('display') !== 'none';
}
-// Some tests/elements can define its own binding. We want to restore bindings
-// at the end of the test. The TestKeyboardShortcutBinder store bindings in
-// stack, so it is possible to override bindings in nested suites.
-export class TestKeyboardShortcutBinder {
- private static stack: TestKeyboardShortcutBinder[] = [];
-
- static push() {
- const testBinder = new TestKeyboardShortcutBinder();
- this.stack.push(testBinder);
- return _testOnly_getShortcutManagerInstance();
- }
-
- static pop() {
- const item = this.stack.pop();
- if (!item) {
- throw new Error('stack is empty');
- }
- item._restoreShortcuts();
- }
-
- private readonly originalBinding: Map<Shortcut, string[]>;
-
- constructor() {
- this.originalBinding = new Map(
- _testOnly_getShortcutManagerInstance()._testOnly_getBindings()
- );
- }
-
- _restoreShortcuts() {
- const bindings =
- _testOnly_getShortcutManagerInstance()._testOnly_getBindings();
- bindings.clear();
- this.originalBinding.forEach((value, key) => {
- bindings.set(key, value);
- });
- }
-}
-
// Provide reset plugins function to clear installed plugins between tests.
// No gr-app found (running tests)
export const resetPlugins = () => {
diff --git a/polygerrit-ui/app/tsconfig.json b/polygerrit-ui/app/tsconfig.json
index 9516dce..5040496 100644
--- a/polygerrit-ui/app/tsconfig.json
+++ b/polygerrit-ui/app/tsconfig.json
@@ -25,6 +25,7 @@
"noUnusedLocals": true, /* Report errors on unused locals. */
"noUnusedParameters": true, /* Report errors on unused parameters. */
"noImplicitReturns": true, /* Report error when not all code paths in function return a value. */
+ "noImplicitOverride": true,
"noFallthroughCasesInSwitch": true,/* Report errors for fallthrough cases in switch statement. */
"skipLibCheck": true, /* Do not check node_modules */
@@ -45,6 +46,31 @@
"typeRoots": [
"node_modules/@types",
"../node_modules/@types"
+ ],
+
+ "plugins": [
+ {
+ "name": "ts-lit-plugin",
+ "strict": true,
+ "rules": {
+ "no-unknown-tag-name": "error",
+ "no-unclosed-tag": "error",
+ "no-unknown-property": "error",
+ "no-unintended-mixed-binding": "error",
+ "no-invalid-boolean-binding": "error",
+ "no-expressionless-property-binding": "error",
+ "no-noncallable-event-binding": "error",
+ "no-boolean-in-attribute-binding": "error",
+ "no-complex-attribute-binding": "error",
+ "no-nullable-attribute-binding": "error",
+ "no-incompatible-type-binding": "error",
+ "no-invalid-directive-binding": "error",
+ "no-incompatible-property-type": "error",
+ "no-unknown-property-converter": "error",
+ "no-invalid-attribute-name": "error",
+ "no-invalid-tag-name": "error"
+ }
+ }
]
},
// With the * pattern (without an extension), only supported files
diff --git a/polygerrit-ui/app/types/events.ts b/polygerrit-ui/app/types/events.ts
index 0069321..c78f61a 100644
--- a/polygerrit-ui/app/types/events.ts
+++ b/polygerrit-ui/app/types/events.ts
@@ -14,7 +14,6 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-import {EventApi} from '@polymer/polymer/lib/legacy/polymer.dom';
import {PatchSetNum} from './common';
import {UIComment} from '../utils/comment-util';
import {FetchRequest} from './types';
@@ -69,10 +68,6 @@
'editable-content-save': EditableContentSaveEvent;
'location-change': LocationChangeEvent;
'iron-announce': IronAnnounceEvent;
- /* prettier-ignore */
- 'keydown': KeydownEvent;
- /* prettier-ignore */
- 'keypress': KeypressEvent;
'line-mouse-enter': LineNumberEvent;
'line-mouse-leave': LineNumberEvent;
'line-cursor-moved-in': LineNumberEvent;
@@ -148,10 +143,6 @@
}
export type IronAnnounceEvent = CustomEvent<IronAnnounceEventDetail>;
-export type KeydownEvent = CustomKeyboardEvent;
-
-export type KeypressEvent = InputEvent;
-
export interface LocationChangeEventDetail {
hash: string;
pathname: string;
@@ -252,20 +243,28 @@
export type TitleChangeEvent = CustomEvent<TitleChangeEventDetail>;
/**
- * Keyboard events emitted from polymer elements.
+ * Keyboard events emitted from elements using IronA11yKeysBehavior: That means
+ * that the element returns a list of handlers from either `keyBindings()` or
+ * from `keyboardShortcuts()`. This event should not be used in Lit elements
+ * and will be obsolete once the Lit migration is completed.
*/
-export interface CustomKeyboardEvent extends CustomEvent, EventApi {
- event: CustomKeyboardEvent;
- detail: {
- keyboardEvent?: CustomKeyboardEvent;
- // TODO(TS): maybe should mark as optional and check before accessing
- key: string;
- };
- readonly altKey: boolean;
- readonly changedTouches: TouchList;
- readonly ctrlKey: boolean;
- readonly metaKey: boolean;
- readonly shiftKey: boolean;
- readonly keyCode: number;
- readonly repeat: boolean;
+export interface IronKeyboardEvent extends CustomEvent {
+ detail: IronKeyboardEventDetail;
+}
+
+export interface IronKeyboardEventDetail {
+ keyboardEvent: KeyboardEvent;
+ key: string;
+ combo?: string;
+}
+
+export function isIronKeyboardEvent(
+ e: IronKeyboardEvent | Event | CustomEvent
+): e is IronKeyboardEvent {
+ const ike = e as IronKeyboardEvent;
+ return !!ike?.detail?.keyboardEvent;
+}
+
+export interface IronKeyboardEventListener {
+ (evt: IronKeyboardEvent): void;
}
diff --git a/polygerrit-ui/app/utils/access-util.ts b/polygerrit-ui/app/utils/access-util.ts
index 44830e2..165eacf 100644
--- a/polygerrit-ui/app/utils/access-util.ts
+++ b/polygerrit-ui/app/utils/access-util.ts
@@ -14,7 +14,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-import {LabelName} from '../types/common';
+import {GitRef, LabelName} from '../types/common';
export enum AccessPermissionId {
ABANDON = 'abandon',
@@ -156,7 +156,7 @@
}
export interface PermissionArrayItem<T> {
- id: string;
+ id: GitRef;
value: T;
}
@@ -175,7 +175,7 @@
return Object.keys(obj)
.map(key => {
return {
- id: key,
+ id: key as GitRef,
value: obj[key],
};
})
diff --git a/polygerrit-ui/app/utils/async-util.ts b/polygerrit-ui/app/utils/async-util.ts
index c82f5e4..90ee5a5 100644
--- a/polygerrit-ui/app/utils/async-util.ts
+++ b/polygerrit-ui/app/utils/async-util.ts
@@ -117,9 +117,9 @@
* Ensure only one call is made within THROTTLE_INTERVAL_MS and any call within
* this interval is ignored
*/
-export function throttleWrap(fn: (e: Event) => void) {
+export function throttleWrap<T>(fn: (e: T) => void) {
let lastCall: number | undefined;
- return (e: Event) => {
+ return (e: T) => {
if (
lastCall !== undefined &&
Date.now() - lastCall < THROTTLE_INTERVAL_MS
diff --git a/polygerrit-ui/app/utils/attention-set-util.ts b/polygerrit-ui/app/utils/attention-set-util.ts
index 631b633..dcd2863 100644
--- a/polygerrit-ui/app/utils/attention-set-util.ts
+++ b/polygerrit-ui/app/utils/attention-set-util.ts
@@ -16,6 +16,7 @@
*/
import {AccountInfo, ChangeInfo, ServerInfo} from '../types/common';
+import {ParsedChangeInfo} from '../types/types';
import {
getAccountTemplate,
isServiceUser,
@@ -29,7 +30,7 @@
export function hasAttention(
account?: AccountInfo,
- change?: ChangeInfo
+ change?: ChangeInfo | ParsedChangeInfo
): boolean {
return (
canHaveAttention(account) &&
@@ -41,7 +42,7 @@
export function getReason(
config?: ServerInfo,
account?: AccountInfo,
- change?: ChangeInfo
+ change?: ChangeInfo | ParsedChangeInfo
) {
if (!hasAttention(account, change)) return '';
if (change?.attention_set === undefined) return '';
diff --git a/polygerrit-ui/app/utils/change-util.ts b/polygerrit-ui/app/utils/change-util.ts
index c94493b..278e7f3 100644
--- a/polygerrit-ui/app/utils/change-util.ts
+++ b/polygerrit-ui/app/utils/change-util.ts
@@ -215,7 +215,7 @@
}
export function isUploader(
- change?: ChangeInfo,
+ change?: ChangeInfo | ParsedChangeInfo,
account?: AccountInfo
): boolean {
if (!change || !account) return false;
@@ -224,7 +224,7 @@
}
export function isInvolved(
- change?: ChangeInfo,
+ change?: ChangeInfo | ParsedChangeInfo,
account?: AccountInfo
): boolean {
const owner = isOwner(change, account);
diff --git a/polygerrit-ui/app/utils/common-util.ts b/polygerrit-ui/app/utils/common-util.ts
index 5370cf9..9e3bc74 100644
--- a/polygerrit-ui/app/utils/common-util.ts
+++ b/polygerrit-ui/app/utils/common-util.ts
@@ -100,16 +100,19 @@
}
export function query<E extends Element = Element>(
- el: Element | undefined,
+ el: Element | null | undefined,
selector: string
): E | undefined {
if (!el) return undefined;
- const root = el.shadowRoot ?? el;
- return root.querySelector<E>(selector) ?? undefined;
+ if (el.shadowRoot) {
+ const r = el.shadowRoot.querySelector<E>(selector);
+ if (r) return r;
+ }
+ return el.querySelector<E>(selector) ?? undefined;
}
export function queryAndAssert<E extends Element = Element>(
- el: Element | undefined,
+ el: Element | null | undefined,
selector: string
): E {
const found = query<E>(el, selector);
diff --git a/polygerrit-ui/app/utils/dom-util.ts b/polygerrit-ui/app/utils/dom-util.ts
index 16129af..ead47bb 100644
--- a/polygerrit-ui/app/utils/dom-util.ts
+++ b/polygerrit-ui/app/utils/dom-util.ts
@@ -14,10 +14,9 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-
-import {dom, EventApi} from '@polymer/polymer/lib/legacy/polymer.dom';
+import {EventApi} from '@polymer/polymer/lib/legacy/polymer.dom';
import {check} from './common-util';
-import {CustomKeyboardEvent} from '../types/events';
+import {IronKeyboardEvent} from '../types/events';
/**
* Event emitted from polymer elements.
@@ -37,6 +36,17 @@
return 'shadowRoot' in el;
}
+export function isElement(node: Node): node is Element {
+ return node.nodeType === 1;
+}
+
+export function isElementTarget(
+ target: EventTarget | null | undefined
+): target is Element {
+ if (!target) return false;
+ return 'nodeType' in target && isElement(target as Node);
+}
+
// TODO: maybe should have a better name for this
function getPathFromNode(el: EventTarget) {
let tagName = '';
@@ -171,7 +181,7 @@
* getEventPath(e); // eg: div.class1>p#pid.class2
* }
*/
-export function getEventPath<T extends PolymerEvent>(e?: T) {
+export function getEventPath<T extends MouseEvent>(e?: T) {
if (!e) return '';
let path = e.composedPath();
@@ -298,23 +308,14 @@
return e.altKey || e.ctrlKey || e.metaKey || e.shiftKey;
}
-// Deprecated. Try using "normal" KeyboardEvent and modifierPressed() above.
-export function isModifierPressed(event: CustomKeyboardEvent) {
- const e = getKeyboardEvent(event);
- return e.altKey || e.ctrlKey || e.metaKey || e.shiftKey;
-}
-
-export function isShiftPressed(event: CustomKeyboardEvent) {
- const e = getKeyboardEvent(event);
+export function shiftPressed(e: KeyboardEvent) {
return e.shiftKey;
}
-export function getKeyboardEvent(e: CustomKeyboardEvent): CustomKeyboardEvent {
- const event = dom(e.detail ? e.detail.keyboardEvent : e);
- // TODO(TS): worth checking if this still holds or not, if no, remove this.
- // When e is a keyboardEvent, e.event is not null.
- if ('event' in event && (event as CustomKeyboardEvent).event) {
- return (event as CustomKeyboardEvent).event;
- }
- return event as CustomKeyboardEvent;
+export function isModifierPressed(e: IronKeyboardEvent) {
+ return modifierPressed(e.detail.keyboardEvent);
+}
+
+export function isShiftPressed(e: IronKeyboardEvent) {
+ return shiftPressed(e.detail.keyboardEvent);
}
diff --git a/polygerrit-ui/app/utils/label-util.ts b/polygerrit-ui/app/utils/label-util.ts
index cde9a45..78c4132 100644
--- a/polygerrit-ui/app/utils/label-util.ts
+++ b/polygerrit-ui/app/utils/label-util.ts
@@ -70,21 +70,33 @@
return max > -min ? max : min;
}
-export function getLabelStatus(
- label?: DetailedLabelInfo,
- vote?: number
-): LabelStatus {
- const value = vote ?? getRepresentativeValue(label);
- const range = getVotingRangeOrDefault(label);
- if (value < 0) {
- return value === range.min ? LabelStatus.REJECTED : LabelStatus.DISLIKED;
- }
- if (value > 0) {
- return value === range.max ? LabelStatus.APPROVED : LabelStatus.RECOMMENDED;
+export function getLabelStatus(label?: LabelInfo, vote?: number): LabelStatus {
+ if (!label) return LabelStatus.NEUTRAL;
+ if (isDetailedLabelInfo(label)) {
+ const value = vote ?? getRepresentativeValue(label);
+ const range = getVotingRangeOrDefault(label);
+ if (value < 0) {
+ return value === range.min ? LabelStatus.REJECTED : LabelStatus.DISLIKED;
+ }
+ if (value > 0) {
+ return value === range.max
+ ? LabelStatus.APPROVED
+ : LabelStatus.RECOMMENDED;
+ }
+ } else if (isQuickLabelInfo(label)) {
+ if (label.approved) return LabelStatus.RECOMMENDED;
+ if (label.rejected) return LabelStatus.DISLIKED;
}
return LabelStatus.NEUTRAL;
}
+export function hasNeutralStatus(
+ label: DetailedLabelInfo,
+ approvalInfo: ApprovalInfo
+) {
+ return getLabelStatus(label, approvalInfo.value) === LabelStatus.NEUTRAL;
+}
+
export function classForLabelStatus(status: LabelStatus) {
switch (status) {
case LabelStatus.APPROVED:
@@ -122,6 +134,17 @@
return label.all?.filter(x => x._account_id === account._account_id)[0];
}
+export function getAllUniqueApprovals(labelInfo?: LabelInfo) {
+ if (!labelInfo || !isDetailedLabelInfo(labelInfo)) return [];
+ const uniqueApprovals = (labelInfo.all ?? [])
+ .filter(
+ (approvalInfo, index, array) =>
+ index === array.findIndex(other => other.value === approvalInfo.value)
+ )
+ .sort((a, b) => -(a.value ?? 0) + (b.value ?? 0));
+ return uniqueApprovals;
+}
+
export function hasVotes(labelInfo: LabelInfo): boolean {
if (isDetailedLabelInfo(labelInfo)) {
return (labelInfo.all ?? []).some(
diff --git a/polygerrit-ui/app/utils/label-util_test.ts b/polygerrit-ui/app/utils/label-util_test.ts
index 196c5e9..1004aac 100644
--- a/polygerrit-ui/app/utils/label-util_test.ts
+++ b/polygerrit-ui/app/utils/label-util_test.ts
@@ -32,8 +32,11 @@
AccountInfo,
ApprovalInfo,
DetailedLabelInfo,
+ LabelInfo,
+ QuickLabelInfo,
} from '../types/common';
import {
+ createAccountWithEmail,
createSubmitRequirementExpressionInfo,
createSubmitRequirementResultInfo,
} from '../test/test-data-generators';
@@ -171,6 +174,25 @@
assert.equal(getLabelStatus(labelInfo), LabelStatus.REJECTED);
});
+ test('getLabelStatus - quicklabelinfo', () => {
+ let labelInfo: QuickLabelInfo = {};
+ assert.equal(getLabelStatus(labelInfo), LabelStatus.NEUTRAL);
+ labelInfo = {approved: createAccountWithEmail()};
+ assert.equal(getLabelStatus(labelInfo), LabelStatus.RECOMMENDED);
+ labelInfo = {rejected: createAccountWithEmail()};
+ assert.equal(getLabelStatus(labelInfo), LabelStatus.DISLIKED);
+ });
+
+ test('getLabelStatus - detailed and quick info', () => {
+ let labelInfo: LabelInfo = {all: [], values: VALUES_2};
+ labelInfo = {
+ all: [{value: 0}],
+ values: VALUES_0,
+ rejected: createAccountWithEmail(),
+ };
+ assert.equal(getLabelStatus(labelInfo), LabelStatus.NEUTRAL);
+ });
+
test('getRepresentativeValue', () => {
let labelInfo: DetailedLabelInfo = {all: []};
assert.equal(getRepresentativeValue(labelInfo), 0);
diff --git a/polygerrit-ui/app/utils/path-list-util.ts b/polygerrit-ui/app/utils/path-list-util.ts
index fd922fc..411421e 100644
--- a/polygerrit-ui/app/utils/path-list-util.ts
+++ b/polygerrit-ui/app/utils/path-list-util.ts
@@ -95,13 +95,13 @@
});
}
-export function computeDisplayPath(path: string) {
+export function computeDisplayPath(path?: string) {
if (path === SpecialFilePath.COMMIT_MESSAGE) {
return 'Commit message';
} else if (path === SpecialFilePath.MERGE_LIST) {
return 'Merge list';
}
- return path;
+ return path ?? '';
}
export function isMagicPath(path?: string) {
diff --git a/polygerrit-ui/app/yarn.lock b/polygerrit-ui/app/yarn.lock
index 395ef64..bfca566 100644
--- a/polygerrit-ui/app/yarn.lock
+++ b/polygerrit-ui/app/yarn.lock
@@ -2,10 +2,10 @@
# yarn lockfile v1
-"@lit/reactive-element@^1.0.0-rc.2":
- version "1.0.0-rc.3"
- resolved "https://registry.yarnpkg.com/@lit/reactive-element/-/reactive-element-1.0.0-rc.3.tgz#5032f493fbf39781b187a7e2dd5d256537c8760c"
- integrity sha512-Rs2px1keOQUNJUo5B+WExl5v244ZNCiN/iMVNO9evFdJjAdWCIupR/p14zRPkNHsciRBELLTcOZ379cI9O6PDg==
+"@lit/reactive-element@^1.0.0":
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/@lit/reactive-element/-/reactive-element-1.0.1.tgz#853cacd4d78d79059f33f66f8e7b0e5c34bee294"
+ integrity sha512-nSD5AA2AZkKuXuvGs8IK7K5ZczLAogfDd26zT9l6S7WzvqALdVWcW5vMUiTnZyj5SPcNwNNANj0koeV1ieqTFQ==
"@mapbox/node-pre-gyp@^1.0.0":
version "1.0.5"
@@ -425,10 +425,10 @@
resolved "https://registry.yarnpkg.com/@types/resize-observer-browser/-/resize-observer-browser-0.1.6.tgz#d8e6c2f830e2650dc06fe74464472ff64b54a302"
integrity sha512-61IfTac0s9jvNtBCpyo86QeaN8qqpMGHdK0uGKCCIy2dt5/Yk84VduHIdWAcmkC5QvdkPL0p5eWYgUZtHKKUVg==
-"@types/trusted-types@^1.0.1":
- version "1.0.6"
- resolved "https://registry.yarnpkg.com/@types/trusted-types/-/trusted-types-1.0.6.tgz#569b8a08121d3203398290d602d84d73c8dcf5da"
- integrity sha512-230RC8sFeHoT6sSUlRO6a8cAnclO06eeiq1QDfiv2FGCLWFvvERWgwIQD4FWqD9A69BN7Lzee4OXwoMVnnsWDw==
+"@types/trusted-types@^2.0.2":
+ version "2.0.2"
+ resolved "https://registry.yarnpkg.com/@types/trusted-types/-/trusted-types-2.0.2.tgz#fc25ad9943bcac11cceb8168db4f275e0e72e756"
+ integrity sha512-F5DIZ36YVLE+PN+Zwws4kJogq47hNgX3Nx6WyDJ3kcplxyke3XIzB8uK5n/Lpm1HBsbGzd6nmGehL8cPekP+Tg==
"@webcomponents/shadycss@^1.10.2", "@webcomponents/shadycss@^1.9.1":
version "1.11.0"
@@ -473,9 +473,9 @@
integrity sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==
are-we-there-yet@~1.1.2:
- version "1.1.5"
- resolved "https://registry.yarnpkg.com/are-we-there-yet/-/are-we-there-yet-1.1.5.tgz#4b35c2944f062a8bfcda66410760350fe9ddfc21"
- integrity sha512-5hYdAkZlcG8tOLujVDTgCT+uPX0VnpAH28gWsLfzpXYm7wP6mp5Q/gYyR7YQ0cKVJcXJnl3j2kpBan13PtQf6w==
+ version "1.1.7"
+ resolved "https://registry.yarnpkg.com/are-we-there-yet/-/are-we-there-yet-1.1.7.tgz#b15474a932adab4ff8a50d9adfa7e4e926f21146"
+ integrity sha512-nxwy40TuMiUGqMyRHgCSWZ9FM4VAoRP4xUYSTv5ImRog+h9yISPbVH7H8fASCIzYn9wlEv4zvFL7uKDMCFQm3g==
dependencies:
delegates "^1.0.0"
readable-stream "^2.0.6"
@@ -518,9 +518,9 @@
integrity sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=
codemirror-minified@^5.62.2:
- version "5.62.2"
- resolved "https://registry.yarnpkg.com/codemirror-minified/-/codemirror-minified-5.62.2.tgz#37d866f5f39bbd4482c60b1607c669bcb7190388"
- integrity sha512-lQpyiEaqyEln1YDiHqq8lJcX8GkTJamecZAn0DkgdteFIVCRHnVmllOXPF+d159OSNkMi1UcKRObcU6ueBHe1A==
+ version "5.63.0"
+ resolved "https://registry.yarnpkg.com/codemirror-minified/-/codemirror-minified-5.63.0.tgz#29d1a78713a633c933a27853679afdc0bfea49cc"
+ integrity sha512-dMN2w0Qg5Zwn2p7UW3sYAoyrJ+QRBkiF5bfbQAvQ1bfqhEjGnZ++/zvOG7NivfnUbYRhSULz8lsFtzt4ldBNyQ==
concat-map@0.0.1:
version "0.0.1"
@@ -533,9 +533,9 @@
integrity sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=
core-util-is@~1.0.0:
- version "1.0.2"
- resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7"
- integrity sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=
+ version "1.0.3"
+ resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.3.tgz#a6042d3634c2b27e9328f837b965fac83808db85"
+ integrity sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==
debug@4:
version "4.3.2"
@@ -588,9 +588,9 @@
wide-align "^1.1.0"
glob@^7.1.3:
- version "7.1.7"
- resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.7.tgz#3b193e9233f01d42d0b3f78294bbeeb418f94a90"
- integrity sha512-OvD9ENzPLbegENnYP5UUfJIirTg4+XwMWGaQfQTY0JenxNvvIKP3U3/tAQSPIu/lHxXYSZmpXlUHeqAIdKzBLQ==
+ version "7.2.0"
+ resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.0.tgz#d15535af7732e02e948f4c41628bd910293f6023"
+ integrity sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==
dependencies:
fs.realpath "^1.0.0"
inflight "^1.0.4"
@@ -613,9 +613,9 @@
debug "4"
immer@^9.0.5:
- version "9.0.5"
- resolved "https://registry.yarnpkg.com/immer/-/immer-9.0.5.tgz#a7154f34fe7064f15f00554cc94c66cc0bf453ec"
- integrity sha512-2WuIehr2y4lmYz9gaQzetPR2ECniCifk4ORaQbU3g5EalLt+0IVTosEPJ5BoYl/75ky2mivzdRzV8wWgQGOSYQ==
+ version "9.0.6"
+ resolved "https://registry.yarnpkg.com/immer/-/immer-9.0.6.tgz#7a96bf2674d06c8143e327cbf73539388ddf1a73"
+ integrity sha512-G95ivKpy+EvVAnAab4fVa4YGYn24J1SpEktnJX7JJ45Bd7xqME/SCplFzYFmTbrkwZbQ4xJK1xMTUYBkN6pWsQ==
inflight@^1.0.4:
version "1.0.6"
@@ -652,29 +652,29 @@
resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11"
integrity sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=
-lit-element@^3.0.0-rc.2:
- version "3.0.0-rc.3"
- resolved "https://registry.yarnpkg.com/lit-element/-/lit-element-3.0.0-rc.3.tgz#cece8f092d28eb6f9c6b23e4138ff5d7260897ef"
- integrity sha512-NDe7yjW18gfYQb1GIEQr1T8sB1GUAb1HB62pdAEw+SK6lUW7OFPKQqCOlRhZ6qJXsw9KxMnyYIprLZT4FZdYdQ==
+lit-element@^3.0.0:
+ version "3.0.1"
+ resolved "https://registry.yarnpkg.com/lit-element/-/lit-element-3.0.1.tgz#3c545af17d8a46268bc1dd5623a47486e6ff76f4"
+ integrity sha512-vs9uybH9ORyK49CFjoNGN85HM9h5bmisU4TQ63phe/+GYlwvY/3SIFYKdjV6xNvzz8v2MnVC+9+QOkPqh+Q3Ew==
dependencies:
- "@lit/reactive-element" "^1.0.0-rc.2"
- lit-html "^2.0.0-rc.4"
+ "@lit/reactive-element" "^1.0.0"
+ lit-html "^2.0.0"
-lit-html@^2.0.0-rc.4:
- version "2.0.0-rc.4"
- resolved "https://registry.yarnpkg.com/lit-html/-/lit-html-2.0.0-rc.4.tgz#1015fa8f1f7c8c5b79999ed0bc11c3b79ff1aab5"
- integrity sha512-WSLGu3vxq7y8q/oOd9I3zxyBELNLLiDk6gAYoKK4PGctI5fbh6lhnO/jVBdy0PV/vTc+cLJCA/occzx3YoNPeg==
+lit-html@^2.0.0:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/lit-html/-/lit-html-2.0.1.tgz#63241015efa07bc9259b6f96f04abd052d2a1f95"
+ integrity sha512-KF5znvFdXbxTYM/GjpdOOnMsjgRcFGusTnB54ixnCTya5zUR0XqrDRj29ybuLS+jLXv1jji6Y8+g4W7WP8uL4w==
dependencies:
- "@types/trusted-types" "^1.0.1"
+ "@types/trusted-types" "^2.0.2"
-lit@2.0.0-rc.3:
- version "2.0.0-rc.3"
- resolved "https://registry.yarnpkg.com/lit/-/lit-2.0.0-rc.3.tgz#8b6a85268aba287c11125dfe57e88e0bc09beaff"
- integrity sha512-UZDLWuspl7saA+WvS0e+TE3NdGGE05hOIwUPTWiibs34c5QupcEzpjB/aElt79V9bELQVNbUUwa0Ow7D1Wuszw==
+lit@2.0.2:
+ version "2.0.2"
+ resolved "https://registry.yarnpkg.com/lit/-/lit-2.0.2.tgz#5e6f422924e0732258629fb379556b6d23f7179c"
+ integrity sha512-hKA/1YaSB+P+DvKWuR2q1Xzy/iayhNrJ3aveD0OQ9CKn6wUjsdnF/7LavDOJsKP/K5jzW/kXsuduPgRvTFrFJw==
dependencies:
- "@lit/reactive-element" "^1.0.0-rc.2"
- lit-element "^3.0.0-rc.2"
- lit-html "^2.0.0-rc.4"
+ "@lit/reactive-element" "^1.0.0"
+ lit-element "^3.0.0"
+ lit-html "^2.0.0"
lru-cache@^6.0.0:
version "6.0.0"
@@ -703,9 +703,9 @@
brace-expansion "^1.1.7"
minipass@^3.0.0:
- version "3.1.3"
- resolved "https://registry.yarnpkg.com/minipass/-/minipass-3.1.3.tgz#7d42ff1f39635482e15f9cdb53184deebd5815fd"
- integrity sha512-Mgd2GdMVzY+x3IJ+oHnVM+KG3lA5c8tnabyJKmHSaG2kAGpudxuOf8ToDkhumF7UzME7DecbQE9uOZhNm7PuJg==
+ version "3.1.5"
+ resolved "https://registry.yarnpkg.com/minipass/-/minipass-3.1.5.tgz#71f6251b0a33a49c01b3cf97ff77eda030dff732"
+ integrity sha512-+8NzxD82XQoNKNrl1d/FSi+X8wAEWR+sbYAfIvub4Nz0d22plFG72CEVVaufV8PNf4qSslFTD8VMOxNVhHCjTw==
dependencies:
yallist "^4.0.0"
@@ -733,9 +733,11 @@
integrity sha512-8ZtvEnA2c5aYCZYd1cvgdnU6cqwixRoYg70xPLWUws5ORTa/lnw+u4amixRS/Ac5U5mQVgp9pnlSUnbNWFaWZQ==
node-fetch@^2.6.1:
- version "2.6.1"
- resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.1.tgz#045bd323631f76ed2e2b55573394416b639a0052"
- integrity sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw==
+ version "2.6.5"
+ resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.5.tgz#42735537d7f080a7e5f78b6c549b7146be1742fd"
+ integrity sha512-mmlIVHJEu5rnIxgEgez6b9GgWXbkZj5YZ7fx+2r94a2E+Uirsp6HsPTPlomfdHtpt/B0cdKviwkoaM6pyvUOpQ==
+ dependencies:
+ whatwg-url "^5.0.0"
nopt@^5.0.0:
version "5.0.0"
@@ -863,9 +865,9 @@
integrity sha1-BF+XgtARrppoA93TgrJDkrPYkPc=
signal-exit@^3.0.0:
- version "3.0.3"
- resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.3.tgz#a1410c2edd8f077b08b4e253c8eacfcaf057461c"
- integrity sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA==
+ version "3.0.5"
+ resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.5.tgz#9e3e8cc0c75a99472b44321033a7702e7738252f"
+ integrity sha512-KWcOiKeQj6ZyXx7zq4YxSMgHRlod4czeBQZrPb8OKcohcqAXShm7E20kEMle9WBt26hFcAf0qLOcp5zmY7kOqQ==
simple-concat@^1.0.0:
version "1.0.1"
@@ -920,9 +922,9 @@
ansi-regex "^3.0.0"
tar@^6.1.0:
- version "6.1.8"
- resolved "https://registry.yarnpkg.com/tar/-/tar-6.1.8.tgz#4fc50cfe56511c538ce15b71e05eebe66530cbd4"
- integrity sha512-sb9b0cp855NbkMJcskdSYA7b11Q8JsX4qe4pyUAfHp+Y6jBjJeek2ZVlwEfWayshEIwlIzXx0Fain3QG9JPm2A==
+ version "6.1.11"
+ resolved "https://registry.yarnpkg.com/tar/-/tar-6.1.11.tgz#6760a38f003afa1b2ffd0ffe9e9abbd0eab3d621"
+ integrity sha512-an/KZQzQUkZCkuoAA64hM92X0Urb6VpRhAFllDzz44U2mcD5scmT3zBc4VgVpkugF580+DQn8eAFSyoQt0tznA==
dependencies:
chownr "^2.0.0"
fs-minipass "^2.0.0"
@@ -931,6 +933,11 @@
mkdirp "^1.0.3"
yallist "^4.0.0"
+tr46@~0.0.3:
+ version "0.0.3"
+ resolved "https://registry.yarnpkg.com/tr46/-/tr46-0.0.3.tgz#8184fd347dac9cdc185992f3a6622e14b9d9ab6a"
+ integrity sha1-gYT9NH2snNwYWZLzpmIuFLnZq2o=
+
tslib@^1.9.0:
version "1.14.1"
resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00"
@@ -941,6 +948,19 @@
resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf"
integrity sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=
+webidl-conversions@^3.0.0:
+ version "3.0.1"
+ resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz#24534275e2a7bc6be7bc86611cc16ae0a5654871"
+ integrity sha1-JFNCdeKnvGvnvIZhHMFq4KVlSHE=
+
+whatwg-url@^5.0.0:
+ version "5.0.0"
+ resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-5.0.0.tgz#966454e8765462e37644d3626f6742ce8b70965d"
+ integrity sha1-lmRU6HZUYuN2RNNib2dCzotwll0=
+ dependencies:
+ tr46 "~0.0.3"
+ webidl-conversions "^3.0.0"
+
wide-align@^1.1.0:
version "1.1.3"
resolved "https://registry.yarnpkg.com/wide-align/-/wide-align-1.1.3.tgz#ae074e6bdc0c14a431e804e624549c633b000457"
diff --git a/resources/com/google/gerrit/server/mail/ChangeHeader.soy b/resources/com/google/gerrit/server/mail/ChangeHeader.soy
index 5dfe671..3d0edab 100644
--- a/resources/com/google/gerrit/server/mail/ChangeHeader.soy
+++ b/resources/com/google/gerrit/server/mail/ChangeHeader.soy
@@ -20,10 +20,10 @@
{@param attentionSet: ?}
{if $attentionSet}
Attention is currently required from:{sp}
- {for $attentionSetUser in $attentionSet}
+ {for $attentionSetUser, $index in $attentionSet}
{$attentionSetUser}
// add commas or dot.
- {if isLast($attentionSetUser)}.
+ {if $index == length($attentionSet) - 1}.
{else},{sp}
{/if}
{/for}
diff --git a/resources/com/google/gerrit/server/mail/ChangeHeaderHtml.soy b/resources/com/google/gerrit/server/mail/ChangeHeaderHtml.soy
index 191737f..0d8da38 100644
--- a/resources/com/google/gerrit/server/mail/ChangeHeaderHtml.soy
+++ b/resources/com/google/gerrit/server/mail/ChangeHeaderHtml.soy
@@ -21,10 +21,10 @@
{@param attentionSet: ?}
{if $attentionSet}
<p> Attention is currently required from:{sp}
- {for $attentionSetUser in $attentionSet}
+ {for $attentionSetUser, $index in $attentionSet}
{$attentionSetUser}
//add commas or dot.
- {if isLast($attentionSetUser)}.
+ {if $index == length($attentionSet) - 1}.
{else},{sp}
{/if}
{/for} </p>
diff --git a/resources/com/google/gerrit/server/mail/Comment.soy b/resources/com/google/gerrit/server/mail/Comment.soy
index 4b923e6..4b66401 100644
--- a/resources/com/google/gerrit/server/mail/Comment.soy
+++ b/resources/com/google/gerrit/server/mail/Comment.soy
@@ -48,8 +48,8 @@
{\n}
{/if}
- {for $line in $comment.lines}
- {if isFirst($line)}
+ {for $line, $index in $comment.lines}
+ {if $index == 0}
{if $comment.startLine != 0}
{$comment.link}
{/if}
diff --git a/resources/com/google/gerrit/server/mail/DeleteReviewer.soy b/resources/com/google/gerrit/server/mail/DeleteReviewer.soy
index a3ed3ee..ae2a9c4 100644
--- a/resources/com/google/gerrit/server/mail/DeleteReviewer.soy
+++ b/resources/com/google/gerrit/server/mail/DeleteReviewer.soy
@@ -26,8 +26,8 @@
{@param email: ?}
{@param fromName: ?}
{$fromName} has removed{sp}
- {for $reviewerName in $email.reviewerNames}
- {if not isFirst($reviewerName)},{sp}{/if}
+ {for $reviewerName, $index in $email.reviewerNames}
+ {if $index > 0},{sp}{/if}
{$reviewerName}
{/for}{sp}
from this change.{sp}
diff --git a/resources/com/google/gerrit/server/mail/DeleteReviewerHtml.soy b/resources/com/google/gerrit/server/mail/DeleteReviewerHtml.soy
index 76a9199..fdcbbe7 100644
--- a/resources/com/google/gerrit/server/mail/DeleteReviewerHtml.soy
+++ b/resources/com/google/gerrit/server/mail/DeleteReviewerHtml.soy
@@ -25,9 +25,9 @@
{$fromName}{sp}
<strong>
removed{sp}
- {for $reviewerName in $email.reviewerNames}
- {if not isFirst($reviewerName)}
- {if isLast($reviewerName)}{sp}and{else},{/if}{sp}
+ {for $reviewerName, $index in $email.reviewerNames}
+ {if $index > 0}
+ {if $index == (length($email.reviewerNames) - 1)}{sp}and{else},{/if}{sp}
{/if}
{$reviewerName}
{/for}
diff --git a/resources/com/google/gerrit/server/mail/NewChange.soy b/resources/com/google/gerrit/server/mail/NewChange.soy
index aa2b946..c5f34b4 100644
--- a/resources/com/google/gerrit/server/mail/NewChange.soy
+++ b/resources/com/google/gerrit/server/mail/NewChange.soy
@@ -30,8 +30,8 @@
{if $email.reviewerNames or $email.removedReviewerNames}
{if $email.reviewerNames}
Hello{sp}
- {for $reviewerName in $email.reviewerNames}
- {if not isFirst($reviewerName)},{sp}{/if}
+ {for $reviewerName, $index in $email.reviewerNames}
+ {if $index > 0},{sp}{/if}
{$reviewerName}
{/for},
@@ -43,8 +43,8 @@
{/if}
{if $email.removedReviewerNames}
{$fromName} has removed{sp}
- {for $reviewerName in $email.removedReviewerNames}
- {if not isFirst($reviewerName)},{sp}{/if}
+ {for $reviewerName, $index in $email.removedReviewerNames}
+ {if $index > 0},{sp}{/if}
{$reviewerName}
{/for}{sp}
from this change.{sp}
diff --git a/resources/com/google/gerrit/server/mail/NewChangeHtml.soy b/resources/com/google/gerrit/server/mail/NewChangeHtml.soy
index 272c3ef..008226f 100644
--- a/resources/com/google/gerrit/server/mail/NewChangeHtml.soy
+++ b/resources/com/google/gerrit/server/mail/NewChangeHtml.soy
@@ -29,9 +29,9 @@
{if $email.reviewerNames or $email.removedReviewerNames}
{if $email.reviewerNames}
{$fromName} would like{sp}
- {for $reviewerName in $email.reviewerNames}
- {if not isFirst($reviewerName)}
- {if isLast($reviewerName)}{sp}and{else},{/if}{sp}
+ {for $reviewerName, $index in $email.reviewerNames}
+ {if $index > 0}
+ {if $index == length($email.reviewerNames) - 1}{sp}and{else},{/if}{sp}
{/if}
{$reviewerName}
{/for}{sp}
@@ -44,9 +44,9 @@
{$fromName}{sp}
<strong>
removed{sp}
- {for $reviewerName in $email.removedReviewerNames}
- {if not isFirst($reviewerName)}
- {if isLast($reviewerName)}{sp}and{else},{/if}{sp}
+ {for $reviewerName, $index in $email.removedReviewerNames}
+ {if $index > 0}
+ {if $index == length($email.removedReviewerNames) - 1}{sp}and{else},{/if}{sp}
{/if}
{$reviewerName}
{/for}
diff --git a/tools/BUILD b/tools/BUILD
index 5af4dae..9f12373 100644
--- a/tools/BUILD
+++ b/tools/BUILD
@@ -202,7 +202,7 @@
"-Xep:FromTemporalAccessor:ERROR",
"-Xep:FunctionalInterfaceClash:ERROR",
"-Xep:FunctionalInterfaceMethodChanged:ERROR",
- "-Xep:FutureReturnValueIgnored:ERROR",
+ # "-Xep:FutureReturnValueIgnored:ERROR", // this check has a bug.
"-Xep:FuturesGetCheckedIllegalExceptionType:ERROR",
"-Xep:GetClassOnAnnotation:ERROR",
"-Xep:GetClassOnClass:ERROR",
diff --git a/twinkie.patch b/twinkie.patch
deleted file mode 100644
index 0a61243..0000000
--- a/twinkie.patch
+++ /dev/null
@@ -1,11 +0,0 @@
---- a/node_modules/twinkie/src/app/index.js
-+++ b/node_modules/twinkie/src/app/index.js
-@@ -250,7 +250,7 @@ twinkie --tsconfig tsconfig.json --outdir output_dir [--files file_list] [--outt
- incremental: false,
- noEmit: true,
- },
-- files: [...allProgramFilesNames, generatedFiles],
-+ files: [...allProgramFilesNames, ...generatedFiles],
- };
- fs.writeFileSync(cmdLineOptions.outputTsConfig, JSON.stringify(tsconfigContent, null, 2));
- }
diff --git a/yarn.lock b/yarn.lock
index 7e13131..3ddfac6 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -30,6 +30,13 @@
chalk "^2.0.0"
js-tokens "^4.0.0"
+"@babel/runtime@^7.10.2":
+ version "7.15.4"
+ resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.15.4.tgz#fd17d16bfdf878e6dd02d19753a39fa8a8d9c84a"
+ integrity sha512-99catp6bHCaxr4sJ/DbTGgHS4+Rs2RVd2g7iOap6SLGPDknRK9ztKNsE/Fg6QhSeh1FGE5f6gHGQmvvn3I3xhw==
+ dependencies:
+ regenerator-runtime "^0.13.4"
+
"@bazel/rollup@^3.5.0":
version "3.8.0"
resolved "https://registry.yarnpkg.com/@bazel/rollup/-/rollup-3.8.0.tgz#850f56176d73e3b7d99a43c7e33df21ecc6ac161"
@@ -79,6 +86,14 @@
resolved "https://registry.yarnpkg.com/@humanwhocodes/object-schema/-/object-schema-1.2.0.tgz#87de7af9c231826fdd68ac7258f77c429e0e5fcf"
integrity sha512-wdppn25U8z/2yiaT6YGquE6X8sSv7hNMWSXYSSU1jGv/yd6XqjXgTDJ8KP4NgjTXfJ3GbRjeeb8RTV7a/VpM+w==
+"@mrmlnc/readdir-enhanced@^2.2.1":
+ version "2.2.1"
+ resolved "https://registry.yarnpkg.com/@mrmlnc/readdir-enhanced/-/readdir-enhanced-2.2.1.tgz#524af240d1a360527b730475ecfa1344aa540dde"
+ integrity sha512-bPHp6Ji8b41szTOcaP63VlnbbO5Ny6dwAATtY6JTjh5N2OLrb5Qk/Th5cRkRQhkWCt+EJsYrNB0MiL+Gpn6e3g==
+ dependencies:
+ call-me-maybe "^1.0.1"
+ glob-to-regexp "^0.3.0"
+
"@nodelib/fs.scandir@2.1.5":
version "2.1.5"
resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz#7619c2eb21b25483f6d167548b4cfd5a7488c3d5"
@@ -92,6 +107,11 @@
resolved "https://registry.yarnpkg.com/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz#5bd262af94e9d25bd1e71b05deed44876a222e8b"
integrity sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==
+"@nodelib/fs.stat@^1.1.2":
+ version "1.1.3"
+ resolved "https://registry.yarnpkg.com/@nodelib/fs.stat/-/fs.stat-1.1.3.tgz#2b5a3ab3f918cca48a8c754c08168e3f03eba61b"
+ integrity sha512-shAmDyaQC4H92APFoIaVDHCx5bStIocgvbwQyxPRrbUY20V1EYTbSDchWbuwlMG3V17cprZhA6+78JfB+3DTPw==
+
"@nodelib/fs.walk@^1.2.3":
version "1.2.8"
resolved "https://registry.yarnpkg.com/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz#e95737e8bb6746ddedf69c556953494f196fe69a"
@@ -191,9 +211,9 @@
integrity sha512-jhuKLIRrhvCPLqwPcx6INqmKeiA5EWrsCOPhrlFSrbrmU4ZMPjj5Ul/oLCMDO98XRUIwVm78xICz4EPCektzeQ==
"@types/node@*":
- version "16.7.10"
- resolved "https://registry.yarnpkg.com/@types/node/-/node-16.7.10.tgz#7aa732cc47341c12a16b7d562f519c2383b6d4fc"
- integrity sha512-S63Dlv4zIPb8x6MMTgDq5WWRJQe56iBEY0O3SOFA9JrRienkOVDXSXBjjJw6HTNQYSE2JI6GMCR6LVbIMHJVvA==
+ version "16.9.6"
+ resolved "https://registry.yarnpkg.com/@types/node/-/node-16.9.6.tgz#040a64d7faf9e5d9e940357125f0963012e66f04"
+ integrity sha512-YHUZhBOMTM3mjFkXVcK+WwAcYmyhe1wL4lfqNtzI0b3qAy7yuSetnM7QJazgE5PFmgVTNGiLOgRFfJMqW7XpSQ==
"@types/node@^10.1.0":
version "10.17.60"
@@ -354,6 +374,21 @@
dependencies:
sprintf-js "~1.0.2"
+arr-diff@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/arr-diff/-/arr-diff-4.0.0.tgz#d6461074febfec71e7e15235761a329a5dc7c520"
+ integrity sha1-1kYQdP6/7HHn4VI1dhoyml3HxSA=
+
+arr-flatten@^1.1.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/arr-flatten/-/arr-flatten-1.1.0.tgz#36048bbff4e7b47e136644316c99669ea5ae91f1"
+ integrity sha512-L3hKV5R/p5o81R7O02IGnwpDmkp6E982XhtbuwSe3O4qOtMMMtodicASA1Cny2U+aCXcNpml+m4dPsvsJ3jatg==
+
+arr-union@^3.1.0:
+ version "3.1.0"
+ resolved "https://registry.yarnpkg.com/arr-union/-/arr-union-3.1.0.tgz#e39b09aea9def866a8f206e288af63919bae39c4"
+ integrity sha1-45sJrqne+Gao8gbiiK9jkZuuOcQ=
+
array-includes@^3.1.3:
version "3.1.3"
resolved "https://registry.yarnpkg.com/array-includes/-/array-includes-3.1.3.tgz#c7f619b382ad2afaf5326cddfdc0afc61af7690a"
@@ -370,6 +405,11 @@
resolved "https://registry.yarnpkg.com/array-union/-/array-union-2.1.0.tgz#b798420adbeb1de828d84acd8a2e23d3efe85e8d"
integrity sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==
+array-unique@^0.3.2:
+ version "0.3.2"
+ resolved "https://registry.yarnpkg.com/array-unique/-/array-unique-0.3.2.tgz#a894b75d4bc4f6cd679ef3244a9fd8f46ae2d428"
+ integrity sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg=
+
array.prototype.flat@^1.2.4:
version "1.2.4"
resolved "https://registry.yarnpkg.com/array.prototype.flat/-/array.prototype.flat-1.2.4.tgz#6ef638b43312bd401b4c6199fdec7e2dc9e9a123"
@@ -384,16 +424,39 @@
resolved "https://registry.yarnpkg.com/arrify/-/arrify-1.0.1.tgz#898508da2226f380df904728456849c1501a4b0d"
integrity sha1-iYUI2iIm84DfkEcoRWhJwVAaSw0=
+assign-symbols@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/assign-symbols/-/assign-symbols-1.0.0.tgz#59667f41fadd4f20ccbc2bb96b8d4f7f78ec0367"
+ integrity sha1-WWZ/QfrdTyDMvCu5a41Pf3jsA2c=
+
astral-regex@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/astral-regex/-/astral-regex-2.0.0.tgz#483143c567aeed4785759c0865786dc77d7d2e31"
integrity sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==
+atob@^2.1.2:
+ version "2.1.2"
+ resolved "https://registry.yarnpkg.com/atob/-/atob-2.1.2.tgz#6d9517eb9e030d2436666651e86bd9f6f13533c9"
+ integrity sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==
+
balanced-match@^1.0.0:
version "1.0.2"
resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee"
integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==
+base@^0.11.1:
+ version "0.11.2"
+ resolved "https://registry.yarnpkg.com/base/-/base-0.11.2.tgz#7bde5ced145b6d551a90db87f83c558b4eb48a8f"
+ integrity sha512-5T6P4xPgpp0YDFvSWwEZ4NoE3aM4QBQXDzmVbraCkFj8zHM+mba8SyqB5DbZWyR7mYHo6Y7BdQo3MoA4m0TeQg==
+ dependencies:
+ cache-base "^1.0.1"
+ class-utils "^0.3.5"
+ component-emitter "^1.2.1"
+ define-property "^1.0.0"
+ isobject "^3.0.1"
+ mixin-deep "^1.2.0"
+ pascalcase "^0.1.1"
+
boolbase@~1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/boolbase/-/boolbase-1.0.0.tgz#68dff5fbe60c51eb37725ea9e3ed310dcc1e776e"
@@ -421,6 +484,22 @@
balanced-match "^1.0.0"
concat-map "0.0.1"
+braces@^2.3.1:
+ version "2.3.2"
+ resolved "https://registry.yarnpkg.com/braces/-/braces-2.3.2.tgz#5979fd3f14cd531565e5fa2df1abfff1dfaee729"
+ integrity sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==
+ dependencies:
+ arr-flatten "^1.1.0"
+ array-unique "^0.3.2"
+ extend-shallow "^2.0.1"
+ fill-range "^4.0.0"
+ isobject "^3.0.1"
+ repeat-element "^1.1.2"
+ snapdragon "^0.8.1"
+ snapdragon-node "^2.0.1"
+ split-string "^3.0.2"
+ to-regex "^3.0.1"
+
braces@^3.0.1:
version "3.0.2"
resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107"
@@ -433,6 +512,21 @@
resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.2.tgz#2b146a6fd72e80b4f55d255f35ed59a3a9a41bd5"
integrity sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==
+cache-base@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/cache-base/-/cache-base-1.0.1.tgz#0a7f46416831c8b662ee36fe4e7c59d76f666ab2"
+ integrity sha512-AKcdTnFSWATd5/GCPRxr2ChwIJ85CeyrEyjRHlKxQ56d4XJMGym0uAiKn0xbLOGOl3+yRpOTi484dVCEc5AUzQ==
+ dependencies:
+ collection-visit "^1.0.0"
+ component-emitter "^1.2.1"
+ get-value "^2.0.6"
+ has-value "^1.0.0"
+ isobject "^3.0.1"
+ set-value "^2.0.0"
+ to-object-path "^0.3.0"
+ union-value "^1.0.0"
+ unset-value "^1.0.0"
+
cacheable-request@^6.0.0:
version "6.1.0"
resolved "https://registry.yarnpkg.com/cacheable-request/-/cacheable-request-6.1.0.tgz#20ffb8bd162ba4be11e9567d823db651052ca912"
@@ -454,6 +548,11 @@
function-bind "^1.1.1"
get-intrinsic "^1.0.2"
+call-me-maybe@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/call-me-maybe/-/call-me-maybe-1.0.1.tgz#26d208ea89e37b5cbde60250a15f031c16a4d66b"
+ integrity sha1-JtII6onje1y95gJQoV8DHBak1ms=
+
callsites@^3.0.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73"
@@ -468,7 +567,7 @@
map-obj "^4.0.0"
quick-lru "^4.0.1"
-camelcase@^5.3.1:
+camelcase@^5.0.0, camelcase@^5.3.1:
version "5.3.1"
resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-5.3.1.tgz#e3c9b31569e106811df242f715725a1f4c494320"
integrity sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==
@@ -478,7 +577,7 @@
resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-6.2.0.tgz#924af881c9d525ac9d87f40d964e5cea982a1809"
integrity sha512-c7wVvbw3f37nuobQNtgsgG9POC9qMbNuMQmTCqZv23b6MIz0fcYpBiOlv9gEN/hdLdnZTDQhg6e9Dq5M1vKvfg==
-chalk@^2.0.0:
+chalk@^2.0.0, chalk@^2.4.2:
version "2.4.2"
resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424"
integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==
@@ -517,6 +616,16 @@
resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-2.0.0.tgz#67a9e964be31a51e15e5010d58e6f12834002f46"
integrity sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ==
+class-utils@^0.3.5:
+ version "0.3.6"
+ resolved "https://registry.yarnpkg.com/class-utils/-/class-utils-0.3.6.tgz#f93369ae8b9a7ce02fd41faad0ca83033190c463"
+ integrity sha512-qOhPa/Fj7s6TY8H8esGu5QNpMMQxz79h+urzrNYN6mn+9BnxlDGf5QZ+XeCDsxSjPqsSR56XOZOJmpeurnLMeg==
+ dependencies:
+ arr-union "^3.1.0"
+ define-property "^0.2.5"
+ isobject "^3.0.0"
+ static-extend "^0.1.1"
+
cli-boxes@^2.2.1:
version "2.2.1"
resolved "https://registry.yarnpkg.com/cli-boxes/-/cli-boxes-2.2.1.tgz#ddd5035d25094fce220e9cab40a45840a440318f"
@@ -534,6 +643,15 @@
resolved "https://registry.yarnpkg.com/cli-width/-/cli-width-3.0.0.tgz#a2f48437a2caa9a22436e794bf071ec9e61cedf6"
integrity sha512-FxqpkPPwu1HjuN93Omfm4h8uIanXofW0RxVEW3k5RKx+mJJYSthzNhp32Kzxxy3YAEZ/Dc/EWN1vZRY0+kOhbw==
+cliui@^6.0.0:
+ version "6.0.0"
+ resolved "https://registry.yarnpkg.com/cliui/-/cliui-6.0.0.tgz#511d702c0c4e41ca156d7d0e96021f23e13225b1"
+ integrity sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==
+ dependencies:
+ string-width "^4.2.0"
+ strip-ansi "^6.0.0"
+ wrap-ansi "^6.2.0"
+
clone-response@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/clone-response/-/clone-response-1.0.2.tgz#d1dc973920314df67fbeb94223b4ee350239e96b"
@@ -541,6 +659,14 @@
dependencies:
mimic-response "^1.0.0"
+collection-visit@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/collection-visit/-/collection-visit-1.0.0.tgz#4bc0373c164bc3291b4d368c829cf1a80a59dca0"
+ integrity sha1-S8A3PBZLwykbTTaMgpzxqApZ3KA=
+ dependencies:
+ map-visit "^1.0.0"
+ object-visit "^1.0.0"
+
color-convert@^1.9.0:
version "1.9.3"
resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8"
@@ -575,6 +701,11 @@
resolved "https://registry.yarnpkg.com/comment-parser/-/comment-parser-1.1.5.tgz#453627ef8f67dbcec44e79a9bd5baa37f0bce9b2"
integrity sha512-RePCE4leIhBlmrqiYTvaqEeGYg7qpSl4etaIabKtdOQVi+mSTIBBklGUwIr79GXYnl3LpMwmDw4KeR2stNc6FA==
+component-emitter@^1.2.1:
+ version "1.3.0"
+ resolved "https://registry.yarnpkg.com/component-emitter/-/component-emitter-1.3.0.tgz#16e4070fba8ae29b679f2215853ee181ab2eabc0"
+ integrity sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg==
+
concat-map@0.0.1:
version "0.0.1"
resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b"
@@ -592,6 +723,11 @@
write-file-atomic "^3.0.0"
xdg-basedir "^4.0.0"
+copy-descriptor@^0.1.0:
+ version "0.1.1"
+ resolved "https://registry.yarnpkg.com/copy-descriptor/-/copy-descriptor-0.1.1.tgz#676f6eb3c39997c2ee1ac3a924fd6124748f578d"
+ integrity sha1-Z29us8OZl8LuGsOpJP1hJHSPV40=
+
cross-spawn@^7.0.2, cross-spawn@^7.0.3:
version "7.0.3"
resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6"
@@ -621,7 +757,7 @@
resolved "https://registry.yarnpkg.com/css-what/-/css-what-2.1.3.tgz#a6d7604573365fe74686c3f311c56513d88285f2"
integrity sha512-a+EPoD+uZiNfh+5fxw2nO9QwFa6nJe2Or35fGY6Ipw1R3R4AGz1d1TEZrCegvw2YTmZ0jXirGYlzxxpYSHwpEg==
-debug@^2.6.9:
+debug@^2.2.0, debug@^2.3.3, debug@^2.6.9:
version "2.6.9"
resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f"
integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==
@@ -655,6 +791,11 @@
resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290"
integrity sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=
+decode-uri-component@^0.2.0:
+ version "0.2.0"
+ resolved "https://registry.yarnpkg.com/decode-uri-component/-/decode-uri-component-0.2.0.tgz#eb3913333458775cb84cd1a1fae062106bb87545"
+ integrity sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU=
+
decompress-response@^3.3.0:
version "3.3.0"
resolved "https://registry.yarnpkg.com/decompress-response/-/decompress-response-3.3.0.tgz#80a4dd323748384bfa248083622aedec982adff3"
@@ -684,6 +825,37 @@
dependencies:
object-keys "^1.0.12"
+define-property@^0.2.5:
+ version "0.2.5"
+ resolved "https://registry.yarnpkg.com/define-property/-/define-property-0.2.5.tgz#c35b1ef918ec3c990f9a5bc57be04aacec5c8116"
+ integrity sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=
+ dependencies:
+ is-descriptor "^0.1.0"
+
+define-property@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/define-property/-/define-property-1.0.0.tgz#769ebaaf3f4a63aad3af9e8d304c9bbe79bfb0e6"
+ integrity sha1-dp66rz9KY6rTr56NMEybvnm/sOY=
+ dependencies:
+ is-descriptor "^1.0.0"
+
+define-property@^2.0.2:
+ version "2.0.2"
+ resolved "https://registry.yarnpkg.com/define-property/-/define-property-2.0.2.tgz#d459689e8d654ba77e02a817f8710d702cb16e9d"
+ integrity sha512-jwK2UV4cnPpbcG7+VRARKTZPUWowwXA8bzH5NP6ud0oeAxyYPuGZUAC7hMugpCdz4BeSZl2Dl9k66CHJ/46ZYQ==
+ dependencies:
+ is-descriptor "^1.0.2"
+ isobject "^3.0.1"
+
+didyoumean2@4.1.0:
+ version "4.1.0"
+ resolved "https://registry.yarnpkg.com/didyoumean2/-/didyoumean2-4.1.0.tgz#f813cb7c82c249443e599be077f76e88f24b85e4"
+ integrity sha512-qTBmfQoXvhKO75D/05C8m+fteQmn4U46FWYiLhXtZQInzitXLWY0EQ/2oKnpAz9g2lQWW8jYcLcT+hPJGT+kig==
+ dependencies:
+ "@babel/runtime" "^7.10.2"
+ leven "^3.1.0"
+ lodash.deburr "^4.1.0"
+
dir-glob@^3.0.1:
version "3.0.1"
resolved "https://registry.yarnpkg.com/dir-glob/-/dir-glob-3.0.1.tgz#56dbf73d992a4a93ba1584f4534063fd2e41717f"
@@ -954,6 +1126,15 @@
semver "^7.3.5"
spdx-expression-parse "^3.0.1"
+eslint-plugin-lit@^1.5.1:
+ version "1.5.1"
+ resolved "https://registry.yarnpkg.com/eslint-plugin-lit/-/eslint-plugin-lit-1.5.1.tgz#e5b86fee4aeb6023ad4bb90b3d9e462ca8eff755"
+ integrity sha512-pYB0QM11uyOk5L55QfGhBmWi8a56PkNsnx+zVpY4bxz9YVquEo4BeRnFmf9AwFyT89rhGud9QruFhM2xJ4piwg==
+ dependencies:
+ parse5 "^6.0.1"
+ parse5-htmlparser2-tree-adapter "^6.0.1"
+ requireindex "^1.2.0"
+
eslint-plugin-node@^11.1.0:
version "11.1.0"
resolved "https://registry.yarnpkg.com/eslint-plugin-node/-/eslint-plugin-node-11.1.0.tgz#c95544416ee4ada26740a30474eefc5402dc671d"
@@ -973,6 +1154,11 @@
dependencies:
prettier-linter-helpers "^1.0.0"
+eslint-plugin-regex@^1.8.0:
+ version "1.8.0"
+ resolved "https://registry.yarnpkg.com/eslint-plugin-regex/-/eslint-plugin-regex-1.8.0.tgz#4bd111cf5235fb76a4a7f77d7ffcb7b3777b8a77"
+ integrity sha512-rmzVvpoxHKgvcYDo9d1X9RMFOtyOV3A6taD3KWE6gIID2dHoc8RPd0YAjDSJ0LG35wnehQBfsNB+F7q4eYqXqw==
+
eslint-scope@^5.1.1:
version "5.1.1"
resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-5.1.1.tgz#e786e59a66cb92b3f6c1fb0d508aab174848f48c"
@@ -1109,6 +1295,34 @@
signal-exit "^3.0.3"
strip-final-newline "^2.0.0"
+expand-brackets@^2.1.4:
+ version "2.1.4"
+ resolved "https://registry.yarnpkg.com/expand-brackets/-/expand-brackets-2.1.4.tgz#b77735e315ce30f6b6eff0f83b04151a22449622"
+ integrity sha1-t3c14xXOMPa27/D4OwQVGiJEliI=
+ dependencies:
+ debug "^2.3.3"
+ define-property "^0.2.5"
+ extend-shallow "^2.0.1"
+ posix-character-classes "^0.1.0"
+ regex-not "^1.0.0"
+ snapdragon "^0.8.1"
+ to-regex "^3.0.1"
+
+extend-shallow@^2.0.1:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/extend-shallow/-/extend-shallow-2.0.1.tgz#51af7d614ad9a9f610ea1bafbb989d6b1c56890f"
+ integrity sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=
+ dependencies:
+ is-extendable "^0.1.0"
+
+extend-shallow@^3.0.0, extend-shallow@^3.0.2:
+ version "3.0.2"
+ resolved "https://registry.yarnpkg.com/extend-shallow/-/extend-shallow-3.0.2.tgz#26a71aaf073b39fb2127172746131c2704028db8"
+ integrity sha1-Jqcarwc7OfshJxcnRhMcJwQCjbg=
+ dependencies:
+ assign-symbols "^1.0.0"
+ is-extendable "^1.0.1"
+
external-editor@^3.0.3:
version "3.1.0"
resolved "https://registry.yarnpkg.com/external-editor/-/external-editor-3.1.0.tgz#cb03f740befae03ea4d283caed2741a83f335495"
@@ -1118,6 +1332,20 @@
iconv-lite "^0.4.24"
tmp "^0.0.33"
+extglob@^2.0.4:
+ version "2.0.4"
+ resolved "https://registry.yarnpkg.com/extglob/-/extglob-2.0.4.tgz#ad00fe4dc612a9232e8718711dc5cb5ab0285543"
+ integrity sha512-Nmb6QXkELsuBr24CJSkilo6UHHgbekK5UiZgfE6UHD3Eb27YC6oD+bhcT+tJ6cl8dmsgdQxnWlcry8ksBIBLpw==
+ dependencies:
+ array-unique "^0.3.2"
+ define-property "^1.0.0"
+ expand-brackets "^2.1.4"
+ extend-shallow "^2.0.1"
+ fragment-cache "^0.2.1"
+ regex-not "^1.0.0"
+ snapdragon "^0.8.1"
+ to-regex "^3.0.1"
+
fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3:
version "3.1.3"
resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525"
@@ -1128,7 +1356,19 @@
resolved "https://registry.yarnpkg.com/fast-diff/-/fast-diff-1.2.0.tgz#73ee11982d86caaf7959828d519cfe927fac5f03"
integrity sha512-xJuoT5+L99XlZ8twedaRf6Ax2TgQVxvgZOYoPKqZufmJib0tL2tegPBOZb1pVNgIhlqDlA0eO0c3wBvQcmzx4w==
-fast-glob@^3.1.1:
+fast-glob@^2.2.6:
+ version "2.2.7"
+ resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-2.2.7.tgz#6953857c3afa475fff92ee6015d52da70a4cd39d"
+ integrity sha512-g1KuQwHOZAmOZMuBtHdxDtju+T2RT8jgCC9aANsbpdiDDTSnjgfuVsIBNKbUeJI3oKMRExcfNDtJl4OhbffMsw==
+ dependencies:
+ "@mrmlnc/readdir-enhanced" "^2.2.1"
+ "@nodelib/fs.stat" "^1.1.2"
+ glob-parent "^3.1.0"
+ is-glob "^4.0.0"
+ merge2 "^1.2.3"
+ micromatch "^3.1.10"
+
+fast-glob@^3.1.1, fast-glob@^3.2.2:
version "3.2.7"
resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.2.7.tgz#fd6cb7a2d7e9aa7a7846111e85a196d6b2f766a1"
integrity sha512-rYGMRwip6lUMvYD3BTScMwT1HtAs2d71SMv66Vrxs0IekGZEjhM0pcMfjQPnknBt2zeCwQMEupiN02ZP4DiT1Q==
@@ -1170,6 +1410,16 @@
dependencies:
flat-cache "^3.0.4"
+fill-range@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-4.0.0.tgz#d544811d428f98eb06a63dc402d2403c328c38f7"
+ integrity sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc=
+ dependencies:
+ extend-shallow "^2.0.1"
+ is-number "^3.0.0"
+ repeat-string "^1.6.1"
+ to-regex-range "^2.1.0"
+
fill-range@^7.0.1:
version "7.0.1"
resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.0.1.tgz#1919a6a7c75fe38b2c7c77e5198535da9acdda40"
@@ -1205,6 +1455,18 @@
resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.2.2.tgz#64bfed5cb68fe3ca78b3eb214ad97b63bedce561"
integrity sha512-JaTY/wtrcSyvXJl4IMFHPKyFur1sE9AUqc0QnhOaJ0CxHtAoIV8pYDzeEfAaNEtGkOfq4gr3LBFmdXW5mOQFnA==
+for-in@^1.0.2:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/for-in/-/for-in-1.0.2.tgz#81068d295a8142ec0ac726c6e2200c30fb6d5e80"
+ integrity sha1-gQaNKVqBQuwKxybG4iAMMPttXoA=
+
+fragment-cache@^0.2.1:
+ version "0.2.1"
+ resolved "https://registry.yarnpkg.com/fragment-cache/-/fragment-cache-0.2.1.tgz#4290fad27f13e89be7f33799c6bc5a0abfff0d19"
+ integrity sha1-QpD60n8T6Jvn8zeZxrxaCr//DRk=
+ dependencies:
+ map-cache "^0.2.2"
+
fs.realpath@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f"
@@ -1225,6 +1487,11 @@
resolved "https://registry.yarnpkg.com/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz#1b0ab3bd553b2a0d6399d29c0e3ea0b252078327"
integrity sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc=
+get-caller-file@^2.0.1:
+ version "2.0.5"
+ resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e"
+ integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==
+
get-intrinsic@^1.0.2, get-intrinsic@^1.1.0, get-intrinsic@^1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.1.1.tgz#15f59f376f855c446963948f0d24cd3637b4abc6"
@@ -1253,6 +1520,19 @@
resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-6.0.1.tgz#a262d8eef67aced57c2852ad6167526a43cbf7b7"
integrity sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==
+get-value@^2.0.3, get-value@^2.0.6:
+ version "2.0.6"
+ resolved "https://registry.yarnpkg.com/get-value/-/get-value-2.0.6.tgz#dc15ca1c672387ca76bd37ac0a395ba2042a2c28"
+ integrity sha1-3BXKHGcjh8p2vTesCjlbogQqLCg=
+
+glob-parent@^3.1.0:
+ version "3.1.0"
+ resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-3.1.0.tgz#9e6af6299d8d3bd2bd40430832bd113df906c5ae"
+ integrity sha1-nmr2KZ2NO9K9QEMIMr0RPfkGxa4=
+ dependencies:
+ is-glob "^3.1.0"
+ path-dirname "^1.0.0"
+
glob-parent@^5.1.2:
version "5.1.2"
resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4"
@@ -1260,6 +1540,11 @@
dependencies:
is-glob "^4.0.1"
+glob-to-regexp@^0.3.0:
+ version "0.3.0"
+ resolved "https://registry.yarnpkg.com/glob-to-regexp/-/glob-to-regexp-0.3.0.tgz#8c5a1494d2066c570cc3bfe4496175acc4d502ab"
+ integrity sha1-jFoUlNIGbFcMw7/kSWF1rMTVAqs=
+
glob@^7.1.3:
version "7.1.7"
resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.7.tgz#3b193e9233f01d42d0b3f78294bbeeb418f94a90"
@@ -1374,6 +1659,37 @@
dependencies:
has-symbols "^1.0.2"
+has-value@^0.3.1:
+ version "0.3.1"
+ resolved "https://registry.yarnpkg.com/has-value/-/has-value-0.3.1.tgz#7b1f58bada62ca827ec0a2078025654845995e1f"
+ integrity sha1-ex9YutpiyoJ+wKIHgCVlSEWZXh8=
+ dependencies:
+ get-value "^2.0.3"
+ has-values "^0.1.4"
+ isobject "^2.0.0"
+
+has-value@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/has-value/-/has-value-1.0.0.tgz#18b281da585b1c5c51def24c930ed29a0be6b177"
+ integrity sha1-GLKB2lhbHFxR3vJMkw7SmgvmsXc=
+ dependencies:
+ get-value "^2.0.6"
+ has-values "^1.0.0"
+ isobject "^3.0.0"
+
+has-values@^0.1.4:
+ version "0.1.4"
+ resolved "https://registry.yarnpkg.com/has-values/-/has-values-0.1.4.tgz#6d61de95d91dfca9b9a02089ad384bff8f62b771"
+ integrity sha1-bWHeldkd/Km5oCCJrThL/49it3E=
+
+has-values@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/has-values/-/has-values-1.0.0.tgz#95b0b63fec2146619a6fe57fe75628d5a39efe4f"
+ integrity sha1-lbC2P+whRmGab+V/51Yo1aOe/k8=
+ dependencies:
+ is-number "^3.0.0"
+ kind-of "^4.0.0"
+
has-yarn@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/has-yarn/-/has-yarn-2.1.0.tgz#137e11354a7b5bf11aa5cb649cf0c6f3ff2b2e77"
@@ -1521,6 +1837,20 @@
has "^1.0.3"
side-channel "^1.0.4"
+is-accessor-descriptor@^0.1.6:
+ version "0.1.6"
+ resolved "https://registry.yarnpkg.com/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz#a9e12cb3ae8d876727eeef3843f8a0897b5c98d6"
+ integrity sha1-qeEss66Nh2cn7u84Q/igiXtcmNY=
+ dependencies:
+ kind-of "^3.0.2"
+
+is-accessor-descriptor@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz#169c2f6d3df1f992618072365c9b0ea1f6878656"
+ integrity sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==
+ dependencies:
+ kind-of "^6.0.0"
+
is-arrayish@^0.2.1:
version "0.2.1"
resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d"
@@ -1541,6 +1871,11 @@
call-bind "^1.0.2"
has-tostringtag "^1.0.0"
+is-buffer@^1.1.5:
+ version "1.1.6"
+ resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be"
+ integrity sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==
+
is-callable@^1.1.4, is-callable@^1.2.3:
version "1.2.4"
resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.2.4.tgz#47301d58dd0259407865547853df6d61fe471945"
@@ -1560,6 +1895,20 @@
dependencies:
has "^1.0.3"
+is-data-descriptor@^0.1.4:
+ version "0.1.4"
+ resolved "https://registry.yarnpkg.com/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz#0b5ee648388e2c860282e793f1856fec3f301b56"
+ integrity sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y=
+ dependencies:
+ kind-of "^3.0.2"
+
+is-data-descriptor@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz#d84876321d0e7add03990406abbbbd36ba9268c7"
+ integrity sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==
+ dependencies:
+ kind-of "^6.0.0"
+
is-date-object@^1.0.1:
version "1.0.5"
resolved "https://registry.yarnpkg.com/is-date-object/-/is-date-object-1.0.5.tgz#0841d5536e724c25597bf6ea62e1bd38298df31f"
@@ -1567,7 +1916,37 @@
dependencies:
has-tostringtag "^1.0.0"
-is-extglob@^2.1.1:
+is-descriptor@^0.1.0:
+ version "0.1.6"
+ resolved "https://registry.yarnpkg.com/is-descriptor/-/is-descriptor-0.1.6.tgz#366d8240dde487ca51823b1ab9f07a10a78251ca"
+ integrity sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==
+ dependencies:
+ is-accessor-descriptor "^0.1.6"
+ is-data-descriptor "^0.1.4"
+ kind-of "^5.0.0"
+
+is-descriptor@^1.0.0, is-descriptor@^1.0.2:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/is-descriptor/-/is-descriptor-1.0.2.tgz#3b159746a66604b04f8c81524ba365c5f14d86ec"
+ integrity sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==
+ dependencies:
+ is-accessor-descriptor "^1.0.0"
+ is-data-descriptor "^1.0.0"
+ kind-of "^6.0.2"
+
+is-extendable@^0.1.0, is-extendable@^0.1.1:
+ version "0.1.1"
+ resolved "https://registry.yarnpkg.com/is-extendable/-/is-extendable-0.1.1.tgz#62b110e289a471418e3ec36a617d472e301dfc89"
+ integrity sha1-YrEQ4omkcUGOPsNqYX1HLjAd/Ik=
+
+is-extendable@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/is-extendable/-/is-extendable-1.0.1.tgz#a7470f9e426733d81bd81e1155264e3a3507cab4"
+ integrity sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==
+ dependencies:
+ is-plain-object "^2.0.4"
+
+is-extglob@^2.1.0, is-extglob@^2.1.1:
version "2.1.1"
resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2"
integrity sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=
@@ -1582,6 +1961,13 @@
resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz#f116f8064fe90b3f7844a38997c0b75051269f1d"
integrity sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==
+is-glob@^3.1.0:
+ version "3.1.0"
+ resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-3.1.0.tgz#7ba5ae24217804ac70707b96922567486cc3e84a"
+ integrity sha1-e6WuJCF4BKxwcHuWkiVnSGzD6Eo=
+ dependencies:
+ is-extglob "^2.1.0"
+
is-glob@^4.0.0, is-glob@^4.0.1:
version "4.0.1"
resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.1.tgz#7567dbe9f2f5e2467bc77ab83c4a29482407a5dc"
@@ -1614,6 +2000,13 @@
dependencies:
has-tostringtag "^1.0.0"
+is-number@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/is-number/-/is-number-3.0.0.tgz#24fd6201a4782cf50561c810276afc7d12d71195"
+ integrity sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=
+ dependencies:
+ kind-of "^3.0.2"
+
is-number@^7.0.0:
version "7.0.0"
resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b"
@@ -1634,6 +2027,13 @@
resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-1.1.0.tgz#71a50c8429dfca773c92a390a4a03b39fcd51d3e"
integrity sha1-caUMhCnfync8kqOQpKA7OfzVHT4=
+is-plain-object@^2.0.3, is-plain-object@^2.0.4:
+ version "2.0.4"
+ resolved "https://registry.yarnpkg.com/is-plain-object/-/is-plain-object-2.0.4.tgz#2c163b3fafb1b606d9d17928f05c2a1c38e07677"
+ integrity sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==
+ dependencies:
+ isobject "^3.0.1"
+
is-regex@^1.1.3:
version "1.1.4"
resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.1.4.tgz#eef5663cd59fa4c0ae339505323df6854bb15958"
@@ -1666,16 +2066,38 @@
resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a"
integrity sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=
+is-windows@^1.0.2:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/is-windows/-/is-windows-1.0.2.tgz#d1850eb9791ecd18e6182ce12a30f396634bb19d"
+ integrity sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==
+
is-yarn-global@^0.3.0:
version "0.3.0"
resolved "https://registry.yarnpkg.com/is-yarn-global/-/is-yarn-global-0.3.0.tgz#d502d3382590ea3004893746754c89139973e232"
integrity sha512-VjSeb/lHmkoyd8ryPVIKvOCn4D1koMqY+vqyjjUfc3xyKtP4dYOxM44sZrnqQSzSds3xyOrUTLTC9LVCVgLngw==
+isarray@1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11"
+ integrity sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=
+
isexe@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10"
integrity sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=
+isobject@^2.0.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/isobject/-/isobject-2.1.0.tgz#f065561096a3f1da2ef46272f815c840d87e0c89"
+ integrity sha1-8GVWEJaj8dou9GJy+BXIQNh+DIk=
+ dependencies:
+ isarray "1.0.0"
+
+isobject@^3.0.0, isobject@^3.0.1:
+ version "3.0.1"
+ resolved "https://registry.yarnpkg.com/isobject/-/isobject-3.0.1.tgz#4e431e92b11a9731636aa1f9c8d1ccbcfdab78df"
+ integrity sha1-TkMekrEalzFjaqH5yNHMvP2reN8=
+
js-tokens@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499"
@@ -1745,7 +2167,26 @@
dependencies:
json-buffer "3.0.0"
-kind-of@^6.0.3:
+kind-of@^3.0.2, kind-of@^3.0.3, kind-of@^3.2.0:
+ version "3.2.2"
+ resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-3.2.2.tgz#31ea21a734bab9bbb0f32466d893aea51e4a3c64"
+ integrity sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=
+ dependencies:
+ is-buffer "^1.1.5"
+
+kind-of@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-4.0.0.tgz#20813df3d712928b207378691a45066fae72dd57"
+ integrity sha1-IIE989cSkosgc3hpGkUGb65y3Vc=
+ dependencies:
+ is-buffer "^1.1.5"
+
+kind-of@^5.0.0:
+ version "5.1.0"
+ resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-5.1.0.tgz#729c91e2d857b7a419a1f9aa65685c4c33f5845d"
+ integrity sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==
+
+kind-of@^6.0.0, kind-of@^6.0.2, kind-of@^6.0.3:
version "6.0.3"
resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-6.0.3.tgz#07c05034a6c349fa06e24fa35aa76db4580ce4dd"
integrity sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==
@@ -1757,6 +2198,11 @@
dependencies:
package-json "^6.3.0"
+leven@^3.1.0:
+ version "3.1.0"
+ resolved "https://registry.yarnpkg.com/leven/-/leven-3.1.0.tgz#77891de834064cccba82ae7842bb6b14a13ed7f2"
+ integrity sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==
+
levn@^0.4.1:
version "0.4.1"
resolved "https://registry.yarnpkg.com/levn/-/levn-0.4.1.tgz#ae4562c007473b932a6200d403268dd2fffc6ade"
@@ -1770,6 +2216,20 @@
resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-1.1.6.tgz#1c00c743b433cd0a4e80758f7b64a57440d9ff00"
integrity sha1-HADHQ7QzzQpOgHWPe2SldEDZ/wA=
+lit-analyzer@1.2.1, lit-analyzer@^1.2.1:
+ version "1.2.1"
+ resolved "https://registry.yarnpkg.com/lit-analyzer/-/lit-analyzer-1.2.1.tgz#725331a4019ae870dd631d4dd709d39a237161ea"
+ integrity sha512-OEARBhDidyaQENavLbzpTKbEmu5rnAI+SdYsH4ia1BlGlLiqQXoym7uH1MaRPtwtUPbkhUfT4OBDZ+74VHc3Cg==
+ dependencies:
+ chalk "^2.4.2"
+ didyoumean2 "4.1.0"
+ fast-glob "^2.2.6"
+ parse5 "5.1.0"
+ ts-simple-type "~1.0.5"
+ vscode-css-languageservice "4.3.0"
+ vscode-html-languageservice "3.1.0"
+ web-component-analyzer "~1.1.1"
+
load-json-file@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/load-json-file/-/load-json-file-4.0.0.tgz#2f5f45ab91e33216234fd53adab668eb4ec0993b"
@@ -1800,6 +2260,11 @@
resolved "https://registry.yarnpkg.com/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz#e23f3f9c4f8fbdde872529c1071857a086e5ccef"
integrity sha1-4j8/nE+Pvd6HJSnBBxhXoIblzO8=
+lodash.deburr@^4.1.0:
+ version "4.1.0"
+ resolved "https://registry.yarnpkg.com/lodash.deburr/-/lodash.deburr-4.1.0.tgz#ddb1bbb3ef07458c0177ba07de14422cb033ff9b"
+ integrity sha1-3bG7s+8HRYwBd7oH3hRCLLAz/5s=
+
lodash.merge@^4.6.2:
version "4.6.2"
resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.2.tgz#558aa53b43b661e1925a0afdfa36a9a1085fe57a"
@@ -1844,6 +2309,11 @@
dependencies:
semver "^6.0.0"
+map-cache@^0.2.2:
+ version "0.2.2"
+ resolved "https://registry.yarnpkg.com/map-cache/-/map-cache-0.2.2.tgz#c32abd0bd6525d9b051645bb4f26ac5dc98a0dbf"
+ integrity sha1-wyq9C9ZSXZsFFkW7TyasXcmKDb8=
+
map-obj@^1.0.0:
version "1.0.1"
resolved "https://registry.yarnpkg.com/map-obj/-/map-obj-1.0.1.tgz#d933ceb9205d82bdcf4886f6742bdc2b4dea146d"
@@ -1854,6 +2324,13 @@
resolved "https://registry.yarnpkg.com/map-obj/-/map-obj-4.2.1.tgz#e4ea399dbc979ae735c83c863dd31bdf364277b7"
integrity sha512-+WA2/1sPmDj1dlvvJmB5G6JKfY9dpn7EVBUL06+y6PoljPkh+6V1QihwxNkbcGxCRjt2b0F9K0taiCuo7MbdFQ==
+map-visit@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/map-visit/-/map-visit-1.0.0.tgz#ecdca8f13144e660f1b5bd41f12f3479d98dfb8f"
+ integrity sha1-7Nyo8TFE5mDxtb1B8S80edmN+48=
+ dependencies:
+ object-visit "^1.0.0"
+
meow@^9.0.0:
version "9.0.0"
resolved "https://registry.yarnpkg.com/meow/-/meow-9.0.0.tgz#cd9510bc5cac9dee7d03c73ee1f9ad959f4ea364"
@@ -1877,11 +2354,30 @@
resolved "https://registry.yarnpkg.com/merge-stream/-/merge-stream-2.0.0.tgz#52823629a14dd00c9770fb6ad47dc6310f2c1f60"
integrity sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==
-merge2@^1.3.0:
+merge2@^1.2.3, merge2@^1.3.0:
version "1.4.1"
resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae"
integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==
+micromatch@^3.1.10:
+ version "3.1.10"
+ resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-3.1.10.tgz#70859bc95c9840952f359a068a3fc49f9ecfac23"
+ integrity sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==
+ dependencies:
+ arr-diff "^4.0.0"
+ array-unique "^0.3.2"
+ braces "^2.3.1"
+ define-property "^2.0.2"
+ extend-shallow "^3.0.2"
+ extglob "^2.0.4"
+ fragment-cache "^0.2.1"
+ kind-of "^6.0.2"
+ nanomatch "^1.2.9"
+ object.pick "^1.3.0"
+ regex-not "^1.0.0"
+ snapdragon "^0.8.1"
+ to-regex "^3.0.2"
+
micromatch@^4.0.4:
version "4.0.4"
resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.4.tgz#896d519dfe9db25fce94ceb7a500919bf881ebf9"
@@ -1933,6 +2429,14 @@
resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602"
integrity sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==
+mixin-deep@^1.2.0:
+ version "1.3.2"
+ resolved "https://registry.yarnpkg.com/mixin-deep/-/mixin-deep-1.3.2.tgz#1120b43dc359a785dce65b55b82e257ccf479566"
+ integrity sha512-WRoDn//mXBiJ1H40rqa3vH0toePwSsGb45iInWlTySa+Uu4k3tYUSxa2v1KqAiLtvlrSzaExqS1gtk96A9zvEA==
+ dependencies:
+ for-in "^1.0.2"
+ is-extendable "^1.0.1"
+
ms@2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8"
@@ -1953,6 +2457,23 @@
resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.8.tgz#1630c42b2251ff81e2a283de96a5497ea92e5e0d"
integrity sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==
+nanomatch@^1.2.9:
+ version "1.2.13"
+ resolved "https://registry.yarnpkg.com/nanomatch/-/nanomatch-1.2.13.tgz#b87a8aa4fc0de8fe6be88895b38983ff265bd119"
+ integrity sha512-fpoe2T0RbHwBTBUOftAfBPaDEi06ufaUai0mE6Yn1kacc3SnTErfb/h+X94VXzI64rKFHYImXSvdwGGCmwOqCA==
+ dependencies:
+ arr-diff "^4.0.0"
+ array-unique "^0.3.2"
+ define-property "^2.0.2"
+ extend-shallow "^3.0.2"
+ fragment-cache "^0.2.1"
+ is-windows "^1.0.2"
+ kind-of "^6.0.2"
+ object.pick "^1.3.0"
+ regex-not "^1.0.0"
+ snapdragon "^0.8.1"
+ to-regex "^3.0.1"
+
natural-compare@^1.4.0:
version "1.4.0"
resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7"
@@ -2002,6 +2523,15 @@
dependencies:
boolbase "~1.0.0"
+object-copy@^0.1.0:
+ version "0.1.0"
+ resolved "https://registry.yarnpkg.com/object-copy/-/object-copy-0.1.0.tgz#7e7d858b781bd7c991a41ba975ed3812754e998c"
+ integrity sha1-fn2Fi3gb18mRpBupde04EnVOmYw=
+ dependencies:
+ copy-descriptor "^0.1.0"
+ define-property "^0.2.5"
+ kind-of "^3.0.3"
+
object-inspect@^1.11.0, object-inspect@^1.9.0:
version "1.11.0"
resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.11.0.tgz#9dceb146cedd4148a0d9e51ab88d34cf509922b1"
@@ -2012,6 +2542,13 @@
resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.1.1.tgz#1c47f272df277f3b1daf061677d9c82e2322c60e"
integrity sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==
+object-visit@^1.0.0:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/object-visit/-/object-visit-1.0.1.tgz#f79c4493af0c5377b59fe39d395e41042dd045bb"
+ integrity sha1-95xEk68MU3e1n+OdOV5BBC3QRbs=
+ dependencies:
+ isobject "^3.0.0"
+
object.assign@^4.1.2:
version "4.1.2"
resolved "https://registry.yarnpkg.com/object.assign/-/object.assign-4.1.2.tgz#0ed54a342eceb37b38ff76eb831a0e788cb63940"
@@ -2022,6 +2559,13 @@
has-symbols "^1.0.1"
object-keys "^1.1.1"
+object.pick@^1.3.0:
+ version "1.3.0"
+ resolved "https://registry.yarnpkg.com/object.pick/-/object.pick-1.3.0.tgz#87a10ac4c1694bd2e1cbf53591a66141fb5dd747"
+ integrity sha1-h6EKxMFpS9Lhy/U1kaZhQftd10c=
+ dependencies:
+ isobject "^3.0.1"
+
object.values@^1.1.4:
version "1.1.4"
resolved "https://registry.yarnpkg.com/object.values/-/object.values-1.1.4.tgz#0d273762833e816b693a637d30073e7051535b30"
@@ -2140,6 +2684,18 @@
json-parse-even-better-errors "^2.3.0"
lines-and-columns "^1.1.6"
+parse5-htmlparser2-tree-adapter@^6.0.1:
+ version "6.0.1"
+ resolved "https://registry.yarnpkg.com/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-6.0.1.tgz#2cdf9ad823321140370d4dbf5d3e92c7c8ddc6e6"
+ integrity sha512-qPuWvbLgvDGilKc5BoicRovlT4MtYT6JfJyBOMDsKoiT+GiuP5qyrPCnR9HcPECIJJmZh5jRndyNThnhhb/vlA==
+ dependencies:
+ parse5 "^6.0.1"
+
+parse5@5.1.0:
+ version "5.1.0"
+ resolved "https://registry.yarnpkg.com/parse5/-/parse5-5.1.0.tgz#c59341c9723f414c452975564c7c00a68d58acd2"
+ integrity sha512-fxNG2sQjHvlVAYmzBZS9YlDp6PTSSDwa98vkD4QgVDDCAo84z5X1t5XyJQ62ImdLXx5NdIIfihey6xpum9/gRQ==
+
parse5@^3.0.1:
version "3.0.3"
resolved "https://registry.yarnpkg.com/parse5/-/parse5-3.0.3.tgz#042f792ffdd36851551cf4e9e066b3874ab45b5c"
@@ -2147,6 +2703,21 @@
dependencies:
"@types/node" "*"
+parse5@^6.0.1:
+ version "6.0.1"
+ resolved "https://registry.yarnpkg.com/parse5/-/parse5-6.0.1.tgz#e1a1c085c569b3dc08321184f19a39cc27f7c30b"
+ integrity sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==
+
+pascalcase@^0.1.1:
+ version "0.1.1"
+ resolved "https://registry.yarnpkg.com/pascalcase/-/pascalcase-0.1.1.tgz#b363e55e8006ca6fe21784d2db22bd15d7917f14"
+ integrity sha1-s2PlXoAGym/iF4TS2yK9FdeRfxQ=
+
+path-dirname@^1.0.0:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/path-dirname/-/path-dirname-1.0.2.tgz#cc33d24d525e099a5388c0336c6e32b9160609e0"
+ integrity sha1-zDPSTVJeCZpTiMAzbG4yuRYGCeA=
+
path-exists@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-3.0.0.tgz#ce0ebeaa5f78cb18925ea7d810d7b59b010fd515"
@@ -2208,6 +2779,11 @@
dependencies:
find-up "^2.1.0"
+posix-character-classes@^0.1.0:
+ version "0.1.1"
+ resolved "https://registry.yarnpkg.com/posix-character-classes/-/posix-character-classes-0.1.1.tgz#01eac0fe3b5af71a2a6c02feabb8c1fef7e00eab"
+ integrity sha1-AerA/jta9xoqbAL+q7jB/vfgDqs=
+
prelude-ls@^1.2.1:
version "1.2.1"
resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396"
@@ -2352,6 +2928,19 @@
indent-string "^4.0.0"
strip-indent "^3.0.0"
+regenerator-runtime@^0.13.4:
+ version "0.13.9"
+ resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.9.tgz#8925742a98ffd90814988d7566ad30ca3b263b52"
+ integrity sha512-p3VT+cOEgxFsRRA9X4lkI1E+k2/CtnKtU4gcxyaCUreilL/vqI6CdZ3wxVUx3UOUg+gnUOQQcRI7BmSI656MYA==
+
+regex-not@^1.0.0, regex-not@^1.0.2:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/regex-not/-/regex-not-1.0.2.tgz#1f4ece27e00b0b65e0247a6810e6a85d83a5752c"
+ integrity sha512-J6SDjUgDxQj5NusnOtdFxDwN/+HWykR8GELwctJ7mdqhcyy1xEc4SRFHUXvxTp661YaVKAjfRLZ9cCqS6tn32A==
+ dependencies:
+ extend-shallow "^3.0.2"
+ safe-regex "^1.1.0"
+
regexpp@^3.0.0, regexpp@^3.1.0:
version "3.2.0"
resolved "https://registry.yarnpkg.com/regexpp/-/regexpp-3.2.0.tgz#0425a2768d8f23bad70ca4b90461fa2f1213e1b2"
@@ -2376,16 +2965,46 @@
dependencies:
rc "^1.2.8"
+repeat-element@^1.1.2:
+ version "1.1.4"
+ resolved "https://registry.yarnpkg.com/repeat-element/-/repeat-element-1.1.4.tgz#be681520847ab58c7568ac75fbfad28ed42d39e9"
+ integrity sha512-LFiNfRcSu7KK3evMyYOuCzv3L10TW7yC1G2/+StMjK8Y6Vqd2MG7r/Qjw4ghtuCOjFvlnms/iMmLqpvW/ES/WQ==
+
+repeat-string@^1.6.1:
+ version "1.6.1"
+ resolved "https://registry.yarnpkg.com/repeat-string/-/repeat-string-1.6.1.tgz#8dcae470e1c88abc2d600fff4a776286da75e637"
+ integrity sha1-jcrkcOHIirwtYA//Sndihtp15jc=
+
+require-directory@^2.1.1:
+ version "2.1.1"
+ resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42"
+ integrity sha1-jGStX9MNqxyXbiNE/+f3kqam30I=
+
require-from-string@^2.0.2:
version "2.0.2"
resolved "https://registry.yarnpkg.com/require-from-string/-/require-from-string-2.0.2.tgz#89a7fdd938261267318eafe14f9c32e598c36909"
integrity sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==
+require-main-filename@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/require-main-filename/-/require-main-filename-2.0.0.tgz#d0b329ecc7cc0f61649f62215be69af54aa8989b"
+ integrity sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==
+
+requireindex@^1.2.0:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/requireindex/-/requireindex-1.2.0.tgz#3463cdb22ee151902635aa6c9535d4de9c2ef1ef"
+ integrity sha512-L9jEkOi3ASd9PYit2cwRfyppc9NoABujTP8/5gFcbERmo5jUoAKovIC3fsF17pkTnGsrByysqX+Kxd2OTNI1ww==
+
resolve-from@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-4.0.0.tgz#4abcd852ad32dd7baabfe9b40e00a36db5f392e6"
integrity sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==
+resolve-url@^0.2.1:
+ version "0.2.1"
+ resolved "https://registry.yarnpkg.com/resolve-url/-/resolve-url-0.2.1.tgz#2c637fe77c893afd2a663fe21aa9080068e2052a"
+ integrity sha1-LGN/53yJOv0qZj/iGqkIAGjiBSo=
+
resolve@^1.10.0, resolve@^1.10.1, resolve@^1.20.0:
version "1.20.0"
resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.20.0.tgz#629a013fb3f70755d6f0b7935cc1c2c5378b1975"
@@ -2409,6 +3028,11 @@
onetime "^5.1.0"
signal-exit "^3.0.2"
+ret@~0.1.10:
+ version "0.1.15"
+ resolved "https://registry.yarnpkg.com/ret/-/ret-0.1.15.tgz#b8a4825d5bdb1fc3f6f53c2bc33f81388681c7bc"
+ integrity sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg==
+
reusify@^1.0.4:
version "1.0.4"
resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.0.4.tgz#90da382b1e126efc02146e90845a88db12925d76"
@@ -2452,6 +3076,13 @@
resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6"
integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==
+safe-regex@^1.1.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/safe-regex/-/safe-regex-1.1.0.tgz#40a3669f3b077d1e943d44629e157dd48023bf2e"
+ integrity sha1-QKNmnzsHfR6UPURinhV91IAjvy4=
+ dependencies:
+ ret "~0.1.10"
+
"safer-buffer@>= 2.1.2 < 3":
version "2.1.2"
resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a"
@@ -2486,6 +3117,21 @@
dependencies:
lru-cache "^6.0.0"
+set-blocking@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7"
+ integrity sha1-BF+XgtARrppoA93TgrJDkrPYkPc=
+
+set-value@^2.0.0, set-value@^2.0.1:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/set-value/-/set-value-2.0.1.tgz#a18d40530e6f07de4228c7defe4227af8cad005b"
+ integrity sha512-JxHc1weCN68wRY0fhCoXpyK55m/XPHafOmK4UWD7m2CI14GMcFypt4w/0+NV5f/ZMby2F6S2wwA7fgynh9gWSw==
+ dependencies:
+ extend-shallow "^2.0.1"
+ is-extendable "^0.1.1"
+ is-plain-object "^2.0.3"
+ split-string "^3.0.1"
+
shebang-command@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-2.0.0.tgz#ccd0af4f8835fbdc265b82461aaf0c36663f34ea"
@@ -2526,6 +3172,47 @@
astral-regex "^2.0.0"
is-fullwidth-code-point "^3.0.0"
+snapdragon-node@^2.0.1:
+ version "2.1.1"
+ resolved "https://registry.yarnpkg.com/snapdragon-node/-/snapdragon-node-2.1.1.tgz#6c175f86ff14bdb0724563e8f3c1b021a286853b"
+ integrity sha512-O27l4xaMYt/RSQ5TR3vpWCAB5Kb/czIcqUFOM/C4fYcLnbZUc1PkjTAMjof2pBWaSTwOUd6qUHcFGVGj7aIwnw==
+ dependencies:
+ define-property "^1.0.0"
+ isobject "^3.0.0"
+ snapdragon-util "^3.0.1"
+
+snapdragon-util@^3.0.1:
+ version "3.0.1"
+ resolved "https://registry.yarnpkg.com/snapdragon-util/-/snapdragon-util-3.0.1.tgz#f956479486f2acd79700693f6f7b805e45ab56e2"
+ integrity sha512-mbKkMdQKsjX4BAL4bRYTj21edOf8cN7XHdYUJEe+Zn99hVEYcMvKPct1IqNe7+AZPirn8BCDOQBHQZknqmKlZQ==
+ dependencies:
+ kind-of "^3.2.0"
+
+snapdragon@^0.8.1:
+ version "0.8.2"
+ resolved "https://registry.yarnpkg.com/snapdragon/-/snapdragon-0.8.2.tgz#64922e7c565b0e14204ba1aa7d6964278d25182d"
+ integrity sha512-FtyOnWN/wCHTVXOMwvSv26d+ko5vWlIDD6zoUJ7LW8vh+ZBC8QdljveRP+crNrtBwioEUWy/4dMtbBjA4ioNlg==
+ dependencies:
+ base "^0.11.1"
+ debug "^2.2.0"
+ define-property "^0.2.5"
+ extend-shallow "^2.0.1"
+ map-cache "^0.2.2"
+ source-map "^0.5.6"
+ source-map-resolve "^0.5.0"
+ use "^3.1.0"
+
+source-map-resolve@^0.5.0:
+ version "0.5.3"
+ resolved "https://registry.yarnpkg.com/source-map-resolve/-/source-map-resolve-0.5.3.tgz#190866bece7553e1f8f267a2ee82c606b5509a1a"
+ integrity sha512-Htz+RnsXWk5+P2slx5Jh3Q66vhQj1Cllm0zvnaY98+NFx+Dv2CF/f5O/t8x+KaNdrdIAsruNzoh/KpialbqAnw==
+ dependencies:
+ atob "^2.1.2"
+ decode-uri-component "^0.2.0"
+ resolve-url "^0.2.1"
+ source-map-url "^0.4.0"
+ urix "^0.1.0"
+
source-map-support@0.5.9:
version "0.5.9"
resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.9.tgz#41bc953b2534267ea2d605bccfa7bfa3111ced5f"
@@ -2542,6 +3229,16 @@
buffer-from "^1.0.0"
source-map "^0.6.0"
+source-map-url@^0.4.0:
+ version "0.4.1"
+ resolved "https://registry.yarnpkg.com/source-map-url/-/source-map-url-0.4.1.tgz#0af66605a745a5a2f91cf1bbf8a7afbc283dec56"
+ integrity sha512-cPiFOTLUKvJFIg4SKVScy4ilPPW6rFgMgfuZJPNoDuMs3nC1HbMUycBoJw77xFIp6z1UJQJOfx6C9GMH80DiTw==
+
+source-map@^0.5.6:
+ version "0.5.7"
+ resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc"
+ integrity sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=
+
source-map@^0.6.0:
version "0.6.1"
resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263"
@@ -2578,11 +3275,26 @@
resolved "https://registry.yarnpkg.com/spdx-license-ids/-/spdx-license-ids-3.0.10.tgz#0d9becccde7003d6c658d487dd48a32f0bf3014b"
integrity sha512-oie3/+gKf7QtpitB0LYLETe+k8SifzsX4KixvpOsbI6S0kRiRQ5MKOio8eMSAKQ17N06+wdEOXRiId+zOxo0hA==
+split-string@^3.0.1, split-string@^3.0.2:
+ version "3.1.0"
+ resolved "https://registry.yarnpkg.com/split-string/-/split-string-3.1.0.tgz#7cb09dda3a86585705c64b39a6466038682e8fe2"
+ integrity sha512-NzNVhJDYpwceVVii8/Hu6DKfD2G+NrQHlS/V/qgv763EYudVwEcMQNxd2lh+0VrUByXN/oJkl5grOhYWvQUYiw==
+ dependencies:
+ extend-shallow "^3.0.0"
+
sprintf-js@~1.0.2:
version "1.0.3"
resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c"
integrity sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=
+static-extend@^0.1.1:
+ version "0.1.2"
+ resolved "https://registry.yarnpkg.com/static-extend/-/static-extend-0.1.2.tgz#60809c39cbff55337226fd5e0b520f341f1fb5c6"
+ integrity sha1-YICcOcv/VTNyJv1eC1IPNB8ftcY=
+ dependencies:
+ define-property "^0.2.5"
+ object-copy "^0.1.0"
+
string-width@^3.0.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/string-width/-/string-width-3.1.0.tgz#22767be21b62af1081574306f69ac51b62203961"
@@ -2717,11 +3429,26 @@
dependencies:
os-tmpdir "~1.0.2"
+to-object-path@^0.3.0:
+ version "0.3.0"
+ resolved "https://registry.yarnpkg.com/to-object-path/-/to-object-path-0.3.0.tgz#297588b7b0e7e0ac08e04e672f85c1f4999e17af"
+ integrity sha1-KXWIt7Dn4KwI4E5nL4XB9JmeF68=
+ dependencies:
+ kind-of "^3.0.2"
+
to-readable-stream@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/to-readable-stream/-/to-readable-stream-1.0.0.tgz#ce0aa0c2f3df6adf852efb404a783e77c0475771"
integrity sha512-Iq25XBt6zD5npPhlLVXGFN3/gyR2/qODcKNNyTMd4vbm39HUaOiAM4PMq0eMVC/Tkxz+Zjdsc55g9yyz+Yq00Q==
+to-regex-range@^2.1.0:
+ version "2.1.1"
+ resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-2.1.1.tgz#7c80c17b9dfebe599e27367e0d4dd5590141db38"
+ integrity sha1-fIDBe53+vlmeJzZ+DU3VWQFB2zg=
+ dependencies:
+ is-number "^3.0.0"
+ repeat-string "^1.6.1"
+
to-regex-range@^5.0.1:
version "5.0.1"
resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-5.0.1.tgz#1648c44aae7c8d988a326018ed72f5b4dd0392e4"
@@ -2729,11 +3456,33 @@
dependencies:
is-number "^7.0.0"
+to-regex@^3.0.1, to-regex@^3.0.2:
+ version "3.0.2"
+ resolved "https://registry.yarnpkg.com/to-regex/-/to-regex-3.0.2.tgz#13cfdd9b336552f30b51f33a8ae1b42a7a7599ce"
+ integrity sha512-FWtleNAtZ/Ki2qtqej2CXTOayOH9bHDQF+Q48VpWyDXjbYxA4Yz8iDB31zXOBUlOHHKidDbqGVrTUvQMPmBGBw==
+ dependencies:
+ define-property "^2.0.2"
+ extend-shallow "^3.0.2"
+ regex-not "^1.0.2"
+ safe-regex "^1.1.0"
+
trim-newlines@^3.0.0:
version "3.0.1"
resolved "https://registry.yarnpkg.com/trim-newlines/-/trim-newlines-3.0.1.tgz#260a5d962d8b752425b32f3a7db0dcacd176c144"
integrity sha512-c1PTsA3tYrIsLGkJkzHF+w9F2EyxfXGo4UyJc4pFL++FMjnq0HJS69T3M7d//gKrFKwy429bouPescbjecU+Zw==
+ts-lit-plugin@^1.2.1:
+ version "1.2.1"
+ resolved "https://registry.yarnpkg.com/ts-lit-plugin/-/ts-lit-plugin-1.2.1.tgz#7fca17a454645c14911917fa7f17ade582fa3056"
+ integrity sha512-k/Me+aT1N9ckC/KuJCAlAJgCHFezOxuOGOzBE0q42xnKbJnUMNl08WqWF6C7OKecCPHIMRk5Wj5o6MDsmt9+qA==
+ dependencies:
+ lit-analyzer "1.2.1"
+
+ts-simple-type@~1.0.5:
+ version "1.0.7"
+ resolved "https://registry.yarnpkg.com/ts-simple-type/-/ts-simple-type-1.0.7.tgz#03930af557528dd40eaa121913c7035a0baaacf8"
+ integrity sha512-zKmsCQs4dZaeSKjEA7pLFDv7FHHqAFLPd0Mr//OIJvu8M+4p4bgSFJwZSEBEg3ec9W7RzRz1vi8giiX0+mheBQ==
+
tsconfig-paths@^3.11.0:
version "3.11.0"
resolved "https://registry.yarnpkg.com/tsconfig-paths/-/tsconfig-paths-3.11.0.tgz#954c1fe973da6339c78e06b03ce2e48810b65f36"
@@ -2763,10 +3512,10 @@
dependencies:
tslib "^1.8.1"
-twinkie@^1.1.2:
- version "1.1.2"
- resolved "https://registry.yarnpkg.com/twinkie/-/twinkie-1.1.2.tgz#c301e4fc26d00d61d3d7e5be030dc6a2264271da"
- integrity sha512-4KwhyrcrRb0WWJKMX/aT+npmMZC0h+sA//+bLhNupmuKvesrH2vEZDe6yIr48FMWKEsdA2xNdQqw/3MapZ5qXQ==
+twinkie@^1.1.3:
+ version "1.1.3"
+ resolved "https://registry.yarnpkg.com/twinkie/-/twinkie-1.1.3.tgz#1a6f0cd11c59e245bc2d16c7c9fc1ec13e477229"
+ integrity sha512-8Y1U/UCtf8JC4snuV4KAo4e9nwJcKZUoMVOApihJzua4JJWeGB/2RYqAusKk3cUczJeZRGzirHpP1hkArcbA8A==
dependencies:
"@types/minimatch" "3.0.3"
cheerio "1.0.0-rc.2"
@@ -2812,16 +3561,16 @@
dependencies:
is-typedarray "^1.0.0"
-typescript@4.0.5:
- version "4.0.5"
- resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.0.5.tgz#ae9dddfd1069f1cb5beb3ef3b2170dd7c1332389"
- integrity sha512-ywmr/VrTVCmNTJ6iV2LwIrfG1P+lv6luD8sUJs+2eI9NLGigaN+nUQc13iHqisq7bra9lnmUSYqbJvegraBOPQ==
-
-typescript@4.3.2:
+typescript@4.0.5, typescript@4.3.2:
version "4.3.2"
resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.3.2.tgz#399ab18aac45802d6f2498de5054fcbbe716a805"
integrity sha512-zZ4hShnmnoVnAHpVHWpTcxdv7dWP60S2FsydQLV8V5PbS3FifjWFFRiHSWpDJahly88PRyV5teTSLoq4eG7mKw==
+typescript@^3.8.3:
+ version "3.9.10"
+ resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.9.10.tgz#70f3910ac7a51ed6bef79da7800690b19bf778b8"
+ integrity sha512-w6fIxVE/H1PkLKcCPsFqKE7Kv7QUwhU8qQY2MueZXWx5cPZdwFupLgKK3vntcK98BtNHZtAF4LA/yl2a7k8R6Q==
+
unbox-primitive@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/unbox-primitive/-/unbox-primitive-1.0.1.tgz#085e215625ec3162574dc8859abee78a59b14471"
@@ -2832,6 +3581,16 @@
has-symbols "^1.0.2"
which-boxed-primitive "^1.0.2"
+union-value@^1.0.0:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/union-value/-/union-value-1.0.1.tgz#0b6fe7b835aecda61c6ea4d4f02c14221e109847"
+ integrity sha512-tJfXmxMeWYnczCVs7XAEvIV7ieppALdyepWMkHkwciRpZraG/xwT+s2JN8+pr1+8jCRf80FFzvr+MpQeeoF4Xg==
+ dependencies:
+ arr-union "^3.1.0"
+ get-value "^2.0.6"
+ is-extendable "^0.1.1"
+ set-value "^2.0.1"
+
unique-string@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/unique-string/-/unique-string-2.0.0.tgz#39c6451f81afb2749de2b233e3f7c5e8843bd89d"
@@ -2839,6 +3598,14 @@
dependencies:
crypto-random-string "^2.0.0"
+unset-value@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/unset-value/-/unset-value-1.0.0.tgz#8376873f7d2335179ffb1e6fc3a8ed0dfc8ab559"
+ integrity sha1-g3aHP30jNRef+x5vw6jtDfyKtVk=
+ dependencies:
+ has-value "^0.3.1"
+ isobject "^3.0.0"
+
update-notifier@^5.0.0:
version "5.1.0"
resolved "https://registry.yarnpkg.com/update-notifier/-/update-notifier-5.1.0.tgz#4ab0d7c7f36a231dd7316cf7729313f0214d9ad9"
@@ -2866,6 +3633,11 @@
dependencies:
punycode "^2.1.0"
+urix@^0.1.0:
+ version "0.1.0"
+ resolved "https://registry.yarnpkg.com/urix/-/urix-0.1.0.tgz#da937f7a62e21fec1fd18d49b35c2935067a6c72"
+ integrity sha1-2pN/emLiH+wf0Y1Js1wpNQZ6bHI=
+
url-parse-lax@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/url-parse-lax/-/url-parse-lax-3.0.0.tgz#16b5cafc07dbe3676c1b1999177823d6503acb0c"
@@ -2873,6 +3645,11 @@
dependencies:
prepend-http "^2.0.0"
+use@^3.1.0:
+ version "3.1.1"
+ resolved "https://registry.yarnpkg.com/use/-/use-3.1.1.tgz#d50c8cac79a19fbc20f2911f56eb973f4e10070f"
+ integrity sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ==
+
util-deprecate@^1.0.1:
version "1.0.2"
resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf"
@@ -2891,6 +3668,56 @@
spdx-correct "^3.0.0"
spdx-expression-parse "^3.0.0"
+vscode-css-languageservice@4.3.0:
+ version "4.3.0"
+ resolved "https://registry.yarnpkg.com/vscode-css-languageservice/-/vscode-css-languageservice-4.3.0.tgz#40c797d664ab6188cace33cfbb19b037580a9318"
+ integrity sha512-BkQAMz4oVHjr0oOAz5PdeE72txlLQK7NIwzmclfr+b6fj6I8POwB+VoXvrZLTbWt9hWRgfvgiQRkh5JwrjPJ5A==
+ dependencies:
+ vscode-languageserver-textdocument "^1.0.1"
+ vscode-languageserver-types "3.16.0-next.2"
+ vscode-nls "^4.1.2"
+ vscode-uri "^2.1.2"
+
+vscode-html-languageservice@3.1.0:
+ version "3.1.0"
+ resolved "https://registry.yarnpkg.com/vscode-html-languageservice/-/vscode-html-languageservice-3.1.0.tgz#265b53bda595e6947b16b0fb8c604e1e58685393"
+ integrity sha512-QAyRHI98bbEIBCqTzZVA0VblGU40na0txggongw5ZgTj9UVsVk5XbLT16O9OTcbqBGSqn0oWmFDNjK/XGIDcqg==
+ dependencies:
+ vscode-languageserver-textdocument "^1.0.1"
+ vscode-languageserver-types "3.16.0-next.2"
+ vscode-nls "^4.1.2"
+ vscode-uri "^2.1.2"
+
+vscode-languageserver-textdocument@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/vscode-languageserver-textdocument/-/vscode-languageserver-textdocument-1.0.1.tgz#178168e87efad6171b372add1dea34f53e5d330f"
+ integrity sha512-UIcJDjX7IFkck7cSkNNyzIz5FyvpQfY7sdzVy+wkKN/BLaD4DQ0ppXQrKePomCxTS7RrolK1I0pey0bG9eh8dA==
+
+vscode-languageserver-types@3.16.0-next.2:
+ version "3.16.0-next.2"
+ resolved "https://registry.yarnpkg.com/vscode-languageserver-types/-/vscode-languageserver-types-3.16.0-next.2.tgz#940bd15c992295a65eae8ab6b8568a1e8daa3083"
+ integrity sha512-QjXB7CKIfFzKbiCJC4OWC8xUncLsxo19FzGVp/ADFvvi87PlmBSCAtZI5xwGjF5qE0xkLf0jjKUn3DzmpDP52Q==
+
+vscode-nls@^4.1.2:
+ version "4.1.2"
+ resolved "https://registry.yarnpkg.com/vscode-nls/-/vscode-nls-4.1.2.tgz#ca8bf8bb82a0987b32801f9fddfdd2fb9fd3c167"
+ integrity sha512-7bOHxPsfyuCqmP+hZXscLhiHwe7CSuFE4hyhbs22xPIhQ4jv99FcR4eBzfYYVLP356HNFpdvz63FFb/xw6T4Iw==
+
+vscode-uri@^2.1.2:
+ version "2.1.2"
+ resolved "https://registry.yarnpkg.com/vscode-uri/-/vscode-uri-2.1.2.tgz#c8d40de93eb57af31f3c715dd650e2ca2c096f1c"
+ integrity sha512-8TEXQxlldWAuIODdukIb+TR5s+9Ds40eSJrw+1iDDA9IFORPjMELarNQE3myz5XIkWWpdprmJjm1/SxMlWOC8A==
+
+web-component-analyzer@~1.1.1:
+ version "1.1.6"
+ resolved "https://registry.yarnpkg.com/web-component-analyzer/-/web-component-analyzer-1.1.6.tgz#d9bd904d904a711c19ba6046a45b60a7ee3ed2e9"
+ integrity sha512-1PyBkb/jijDEVE+Pnk3DTmVHD8takipdvAwvZv1V8jIidsSIJ5nhN87Gs+4dpEb1vw48yp8dnbZKkvMYJ+C0VQ==
+ dependencies:
+ fast-glob "^3.2.2"
+ ts-simple-type "~1.0.5"
+ typescript "^3.8.3"
+ yargs "^15.3.1"
+
which-boxed-primitive@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz#13757bc89b209b049fe5d86430e21cf40a89a8e6"
@@ -2902,6 +3729,11 @@
is-string "^1.0.5"
is-symbol "^1.0.3"
+which-module@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/which-module/-/which-module-2.0.0.tgz#d9ef07dce77b9902b8a3a8fa4b31c3e3f7e6e87a"
+ integrity sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=
+
which@^2.0.1:
version "2.0.2"
resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1"
@@ -2921,6 +3753,15 @@
resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.3.tgz#610636f6b1f703891bd34771ccb17fb93b47079c"
integrity sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==
+wrap-ansi@^6.2.0:
+ version "6.2.0"
+ resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-6.2.0.tgz#e9393ba07102e6c91a3b221478f0257cd2856e53"
+ integrity sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==
+ dependencies:
+ ansi-styles "^4.0.0"
+ string-width "^4.1.0"
+ strip-ansi "^6.0.0"
+
wrap-ansi@^7.0.0:
version "7.0.0"
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
@@ -2950,12 +3791,42 @@
resolved "https://registry.yarnpkg.com/xdg-basedir/-/xdg-basedir-4.0.0.tgz#4bc8d9984403696225ef83a1573cbbcb4e79db13"
integrity sha512-PSNhEJDejZYV7h50BohL09Er9VaIefr2LMAf3OEmpCkjOi34eYyQYAXUTjEQtZJTKcF0E2UKTh+osDLsgNim9Q==
+y18n@^4.0.0:
+ version "4.0.3"
+ resolved "https://registry.yarnpkg.com/y18n/-/y18n-4.0.3.tgz#b5f259c82cd6e336921efd7bfd8bf560de9eeedf"
+ integrity sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==
+
yallist@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72"
integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==
+yargs-parser@^18.1.2:
+ version "18.1.3"
+ resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-18.1.3.tgz#be68c4975c6b2abf469236b0c870362fab09a7b0"
+ integrity sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==
+ dependencies:
+ camelcase "^5.0.0"
+ decamelize "^1.2.0"
+
yargs-parser@^20.2.3:
version "20.2.9"
resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-20.2.9.tgz#2eb7dc3b0289718fc295f362753845c41a0c94ee"
integrity sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==
+
+yargs@^15.3.1:
+ version "15.4.1"
+ resolved "https://registry.yarnpkg.com/yargs/-/yargs-15.4.1.tgz#0d87a16de01aee9d8bec2bfbf74f67851730f4f8"
+ integrity sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==
+ dependencies:
+ cliui "^6.0.0"
+ decamelize "^1.2.0"
+ find-up "^4.1.0"
+ get-caller-file "^2.0.1"
+ require-directory "^2.1.1"
+ require-main-filename "^2.0.0"
+ set-blocking "^2.0.0"
+ string-width "^4.2.0"
+ which-module "^2.0.0"
+ y18n "^4.0.0"
+ yargs-parser "^18.1.2"