Merge changes I022c6a13,I799bf1c5
* changes:
Skip account visibility checks when querying changes
Check permissions when resolving accounts by secondary emails
diff --git a/Documentation/metrics.txt b/Documentation/metrics.txt
index 70352dc..e6494e6 100644
--- a/Documentation/metrics.txt
+++ b/Documentation/metrics.txt
@@ -199,6 +199,14 @@
=== Change
+* `change/count_rebases`: Total number of rebases
+** `on_behalf_of_uploader`:
+ Whether the rebase was done on behalf of the uploader.
+** `rebase_chain`:
+ Whether a chain was rebased.
+* `change/submitted_with_rebaser_approval`: Number of rebased changes that were
+ submitted with a Code-Review approval of the rebaser that would not have been
+ submittable if the rebase was not done on behalf of the uploader.
* `change/submit_rule_evaluation`: Latency for evaluating submit rules on a
change.
* `change/submit_type_evaluation`: Latency for evaluating the submit type on a
diff --git a/Documentation/rest-api-changes.txt b/Documentation/rest-api-changes.txt
index 2810d1e..aceb38e 100644
--- a/Documentation/rest-api-changes.txt
+++ b/Documentation/rest-api-changes.txt
@@ -3799,6 +3799,10 @@
If another user removed a user's vote, the user with the deleted vote will be
added to the attention set.
+The request returns:
+ * '204 No Content' if the vote is deleted successfully;
+ * '404 Not Found' when the vote to be deleted is zero or not present.
+
.Request
----
DELETE /changes/myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940/reviewers/John%20Doe/votes/Code-Review HTTP/1.0
diff --git a/java/com/google/gerrit/acceptance/TestMetricMaker.java b/java/com/google/gerrit/acceptance/TestMetricMaker.java
index 85c5b6d..647eb9d 100644
--- a/java/com/google/gerrit/acceptance/TestMetricMaker.java
+++ b/java/com/google/gerrit/acceptance/TestMetricMaker.java
@@ -14,20 +14,26 @@
package com.google.gerrit.acceptance;
+import com.google.auto.value.AutoValue;
+import com.google.common.collect.ImmutableList;
import com.google.gerrit.metrics.Counter0;
+import com.google.gerrit.metrics.Counter1;
+import com.google.gerrit.metrics.Counter2;
+import com.google.gerrit.metrics.Counter3;
import com.google.gerrit.metrics.Description;
import com.google.gerrit.metrics.DisabledMetricMaker;
+import com.google.gerrit.metrics.Field;
import com.google.inject.Singleton;
+import java.util.Arrays;
import java.util.concurrent.ConcurrentHashMap;
import org.apache.commons.lang3.mutable.MutableLong;
/**
* {@link com.google.gerrit.metrics.MetricMaker} to be bound in tests.
*
- * <p>Records how often {@link Counter0} metrics are invoked. Metrics of other types are not
- * recorded.
+ * <p>Records how often counter metrics are invoked. Metrics of other types are not recorded.
*
- * <p>Allows test to check how much a {@link Counter0} metrics is increased by an operation.
+ * <p>Allows test to check how much a counter metrics is increased by an operation.
*
* <p>Example:
*
@@ -48,18 +54,18 @@
*/
@Singleton
public class TestMetricMaker extends DisabledMetricMaker {
- private final ConcurrentHashMap<String, MutableLong> counts = new ConcurrentHashMap<>();
+ private final ConcurrentHashMap<CounterKey, MutableLong> counts = new ConcurrentHashMap<>();
- public long getCount(String counter0Name) {
- return get(counter0Name).longValue();
+ public long getCount(String counterName, Object... fieldValues) {
+ return get(CounterKey.create(counterName, fieldValues)).longValue();
}
public void reset() {
counts.clear();
}
- private MutableLong get(String counter0Name) {
- return counts.computeIfAbsent(counter0Name, name -> new MutableLong(0));
+ private MutableLong get(CounterKey counterKey) {
+ return counts.computeIfAbsent(counterKey, key -> new MutableLong(0));
}
@Override
@@ -67,11 +73,64 @@
return new Counter0() {
@Override
public void incrementBy(long value) {
- get(name).add(value);
+ get(CounterKey.create(name)).add(value);
}
@Override
public void remove() {}
};
}
+
+ @Override
+ public <F1> Counter1<F1> newCounter(String name, Description desc, Field<F1> field1) {
+ return new Counter1<>() {
+ @Override
+ public void incrementBy(F1 field1, long value) {
+ get(CounterKey.create(name, field1)).add(value);
+ }
+
+ @Override
+ public void remove() {}
+ };
+ }
+
+ @Override
+ public <F1, F2> Counter2<F1, F2> newCounter(
+ String name, Description desc, Field<F1> field1, Field<F2> field2) {
+ return new Counter2<>() {
+ @Override
+ public void incrementBy(F1 field1, F2 field2, long value) {
+ get(CounterKey.create(name, field1, field2)).add(value);
+ }
+
+ @Override
+ public void remove() {}
+ };
+ }
+
+ @Override
+ public <F1, F2, F3> Counter3<F1, F2, F3> newCounter(
+ String name, Description desc, Field<F1> field1, Field<F2> field2, Field<F3> field3) {
+ return new Counter3<>() {
+ @Override
+ public void incrementBy(F1 field1, F2 field2, F3 field3, long value) {
+ get(CounterKey.create(name, field1, field2, field3)).add(value);
+ }
+
+ @Override
+ public void remove() {}
+ };
+ }
+
+ @AutoValue
+ abstract static class CounterKey {
+ abstract String name();
+
+ abstract ImmutableList<Object> fieldValues();
+
+ static CounterKey create(String name, Object... fieldValues) {
+ return new AutoValue_TestMetricMaker_CounterKey(
+ name, ImmutableList.copyOf(Arrays.asList(fieldValues)));
+ }
+ }
}
diff --git a/java/com/google/gerrit/server/mail/send/ChangeEmail.java b/java/com/google/gerrit/server/mail/send/ChangeEmail.java
index 7bbee2a..ff811a0 100644
--- a/java/com/google/gerrit/server/mail/send/ChangeEmail.java
+++ b/java/com/google/gerrit/server/mail/send/ChangeEmail.java
@@ -242,7 +242,9 @@
}
private int getInsertionsCount() {
- return listModifiedFiles().values().stream()
+ return listModifiedFiles().entrySet().stream()
+ .filter(e -> !Patch.COMMIT_MSG.equals(e.getKey()))
+ .map(Map.Entry::getValue)
.map(FileDiffOutput::insertions)
.reduce(0, Integer::sum);
}
@@ -323,8 +325,8 @@
+ "{1,choice,0#0 insertions|1#1 insertion|1<{1} insertions}(+), " //
+ "{2,choice,0#0 deletions|1#1 deletion|1<{2} deletions}(-)" //
+ "\n",
- modifiedFiles.size() - 1, //
- getInsertionsCount(), //
+ modifiedFiles.size() - 1, // -1 to account for the commit message
+ getInsertionsCount(),
getDeletionsCount()));
detail.append("\n");
}
diff --git a/java/com/google/gerrit/server/query/change/ChangePredicates.java b/java/com/google/gerrit/server/query/change/ChangePredicates.java
index 9c340c4..e9bf3c2 100644
--- a/java/com/google/gerrit/server/query/change/ChangePredicates.java
+++ b/java/com/google/gerrit/server/query/change/ChangePredicates.java
@@ -118,8 +118,12 @@
* com.google.gerrit.entities.Change.Id}.
*/
public static Predicate<ChangeData> idStr(Change.Id id) {
+ return idStr(id.toString());
+ }
+
+ public static Predicate<ChangeData> idStr(String id) {
return new ChangeIndexCardinalPredicate(
- ChangeField.NUMERIC_ID_STR_SPEC, ChangeQueryBuilder.FIELD_CHANGE, id.toString(), 1);
+ ChangeField.NUMERIC_ID_STR_SPEC, ChangeQueryBuilder.FIELD_CHANGE, id, 1);
}
/**
diff --git a/java/com/google/gerrit/server/query/change/MagicLabelPredicate.java b/java/com/google/gerrit/server/query/change/MagicLabelPredicate.java
index 32a8fdf..9120069 100644
--- a/java/com/google/gerrit/server/query/change/MagicLabelPredicate.java
+++ b/java/com/google/gerrit/server/query/change/MagicLabelPredicate.java
@@ -99,6 +99,15 @@
return new EqualsLabelPredicate(args, label, value, account, count);
}
+ public String getLabel() {
+ return magicLabelVote.label();
+ }
+
+ public boolean ignoresUploaderApprovals() {
+ return account.equals(ChangeQueryBuilder.NON_UPLOADER_ACCOUNT_ID)
+ || account.equals(ChangeQueryBuilder.NON_CONTRIBUTOR_ACCOUNT_ID);
+ }
+
@Nullable
protected static LabelType type(LabelTypes types, String toFind) {
if (types.byLabel(toFind).isPresent()) {
diff --git a/java/com/google/gerrit/server/restapi/change/DeleteVoteOp.java b/java/com/google/gerrit/server/restapi/change/DeleteVoteOp.java
index 0e1a218..3ac4d22 100644
--- a/java/com/google/gerrit/server/restapi/change/DeleteVoteOp.java
+++ b/java/com/google/gerrit/server/restapi/change/DeleteVoteOp.java
@@ -153,10 +153,12 @@
}
// Set the approval to 0 if vote is being removed.
newApprovals.put(a.label(), (short) 0);
- found = true;
-
- // Set old value, as required by VoteDeleted.
- oldApprovals.put(a.label(), a.value());
+ // If the value is 0, we treat it as already deleted, so no additional actions is required
+ if (a.value() != 0) {
+ found = true;
+ // Set old value, as required by VoteDeleted.
+ oldApprovals.put(a.label(), a.value());
+ }
break;
}
if (!found) {
diff --git a/java/com/google/gerrit/server/restapi/change/Rebase.java b/java/com/google/gerrit/server/restapi/change/Rebase.java
index fd51fbc..5368c75 100644
--- a/java/com/google/gerrit/server/restapi/change/Rebase.java
+++ b/java/com/google/gerrit/server/restapi/change/Rebase.java
@@ -84,6 +84,7 @@
private final PatchSetUtil patchSetUtil;
private final IdentifiedUser.GenericFactory userFactory;
private final ChangeResource.Factory changeResourceFactory;
+ private final RebaseMetrics rebaseMetrics;
@Inject
public Rebase(
@@ -96,7 +97,8 @@
ProjectCache projectCache,
PatchSetUtil patchSetUtil,
IdentifiedUser.GenericFactory userFactory,
- ChangeResource.Factory changeResourceFactory) {
+ ChangeResource.Factory changeResourceFactory,
+ RebaseMetrics rebaseMetrics) {
this.serverIdent = serverIdent;
this.updateFactory = updateFactory;
this.repoManager = repoManager;
@@ -107,6 +109,7 @@
this.patchSetUtil = patchSetUtil;
this.userFactory = userFactory;
this.changeResourceFactory = changeResourceFactory;
+ this.rebaseMetrics = rebaseMetrics;
}
@Override
@@ -147,6 +150,8 @@
bu.addOp(change.getId(), rebaseOp);
bu.execute();
+ rebaseMetrics.countRebase(input.onBehalfOfUploader);
+
ChangeInfo changeInfo = json.create(OPTIONS).format(change.getProject(), change.getId());
changeInfo.containsGitConflicts =
!rebaseOp.getRebasedCommit().getFilesWithGitConflicts().isEmpty() ? true : null;
diff --git a/java/com/google/gerrit/server/restapi/change/RebaseChain.java b/java/com/google/gerrit/server/restapi/change/RebaseChain.java
index 34a2623..1949c89 100644
--- a/java/com/google/gerrit/server/restapi/change/RebaseChain.java
+++ b/java/com/google/gerrit/server/restapi/change/RebaseChain.java
@@ -86,6 +86,7 @@
private final ProjectCache projectCache;
private final PatchSetUtil patchSetUtil;
private final ChangeJson.Factory json;
+ private final RebaseMetrics rebaseMetrics;
@Inject
RebaseChain(
@@ -99,7 +100,8 @@
ChangeNotes.Factory notesFactory,
ProjectCache projectCache,
PatchSetUtil patchSetUtil,
- ChangeJson.Factory json) {
+ ChangeJson.Factory json,
+ RebaseMetrics rebaseMetrics) {
this.repoManager = repoManager;
this.getRelatedChangesUtil = getRelatedChangesUtil;
this.changeDataFactory = changeDataFactory;
@@ -111,6 +113,7 @@
this.projectCache = projectCache;
this.patchSetUtil = patchSetUtil;
this.json = json;
+ this.rebaseMetrics = rebaseMetrics;
}
@Override
@@ -194,6 +197,8 @@
}
}
+ rebaseMetrics.countRebaseChain(input.onBehalfOfUploader);
+
RebaseChainInfo res = new RebaseChainInfo();
res.rebasedChanges = new ArrayList<>();
ChangeJson changeJson = json.create(OPTIONS);
diff --git a/java/com/google/gerrit/server/restapi/change/RebaseMetrics.java b/java/com/google/gerrit/server/restapi/change/RebaseMetrics.java
new file mode 100644
index 0000000..114a112
--- /dev/null
+++ b/java/com/google/gerrit/server/restapi/change/RebaseMetrics.java
@@ -0,0 +1,54 @@
+// Copyright (C) 2023 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.restapi.change;
+
+import com.google.gerrit.metrics.Counter2;
+import com.google.gerrit.metrics.Description;
+import com.google.gerrit.metrics.Field;
+import com.google.gerrit.metrics.MetricMaker;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+
+/** Metrics for the rebase REST endpoints ({@link Rebase} and {@link RebaseChain}). */
+@Singleton
+public class RebaseMetrics {
+ private final Counter2<Boolean, Boolean> countRebases;
+
+ @Inject
+ public RebaseMetrics(MetricMaker metricMaker) {
+ this.countRebases =
+ metricMaker.newCounter(
+ "change/count_rebases",
+ new Description("Total number of rebases").setRate(),
+ Field.ofBoolean("on_behalf_of_uploader", (metadataBuilder, isOnBehalfOfUploader) -> {})
+ .description("Whether the rebase was done on behalf of the uploader.")
+ .build(),
+ Field.ofBoolean("rebase_chain", (metadataBuilder, isRebaseChain) -> {})
+ .description("Whether a chain was rebased.")
+ .build());
+ }
+
+ public void countRebase(boolean isOnBehalfOfUploader) {
+ countRebase(isOnBehalfOfUploader, /* isRebaseChain= */ false);
+ }
+
+ public void countRebaseChain(boolean isOnBehalfOfUploader) {
+ countRebase(isOnBehalfOfUploader, /* isRebaseChain= */ true);
+ }
+
+ private void countRebase(boolean isOnBehalfOfUploader, boolean isRebaseChain) {
+ countRebases.increment(/* field1= */ isOnBehalfOfUploader, /* field2= */ isRebaseChain);
+ }
+}
diff --git a/java/com/google/gerrit/server/submit/MergeMetrics.java b/java/com/google/gerrit/server/submit/MergeMetrics.java
new file mode 100644
index 0000000..9eb8061
--- /dev/null
+++ b/java/com/google/gerrit/server/submit/MergeMetrics.java
@@ -0,0 +1,134 @@
+// Copyright (C) 2023 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.submit;
+
+import com.google.gerrit.entities.SubmitRequirement;
+import com.google.gerrit.index.query.Predicate;
+import com.google.gerrit.index.query.QueryParseException;
+import com.google.gerrit.metrics.Counter0;
+import com.google.gerrit.metrics.Description;
+import com.google.gerrit.metrics.MetricMaker;
+import com.google.gerrit.server.query.change.ChangeData;
+import com.google.gerrit.server.query.change.MagicLabelPredicate;
+import com.google.gerrit.server.query.change.SubmitRequirementChangeQueryBuilder;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+
+/** Metrics are recorded when a change is merged (aka submitted). */
+public class MergeMetrics {
+ private final Provider<SubmitRequirementChangeQueryBuilder> submitRequirementChangequeryBuilder;
+
+ // TODO: This metric is for measuring the impact of allowing users to rebase changes on behalf of
+ // the uploader. Once this feature has been rolled out and its impact as been measured, we may
+ // remove this metric.
+ private final Counter0 countChangesThatWereSubmittedWithRebaserApproval;
+
+ @Inject
+ public MergeMetrics(
+ Provider<SubmitRequirementChangeQueryBuilder> submitRequirementChangequeryBuilder,
+ MetricMaker metricMaker) {
+ this.submitRequirementChangequeryBuilder = submitRequirementChangequeryBuilder;
+
+ this.countChangesThatWereSubmittedWithRebaserApproval =
+ metricMaker.newCounter(
+ "change/submitted_with_rebaser_approval",
+ new Description(
+ "Number of rebased changes that were submitted with a Code-Review approval of"
+ + " the rebaser that would not have been submittable if the rebase was not"
+ + " done on behalf of the uploader.")
+ .setRate());
+ }
+
+ public void countChangesThatWereSubmittedWithRebaserApproval(ChangeData cd) {
+ if (isRebaseOnBehalfOfUploader(cd)
+ && hasCodeReviewApprovalOfRealUploader(cd)
+ && ignoresCodeReviewApprovalsOfUploader(cd)) {
+ // 1. The patch set that is being submitted was created by rebasing on behalf of the uploader.
+ // The uploader of the patch set is the original uploader on whom's behalf the rebase was
+ // done. The real uploader is the user that did the rebase on behalf of the uploader (e.g. by
+ // clicking on the rebase button).
+ //
+ // 2. The change has Code-Review approvals of the real uploader (aka the rebaser).
+ //
+ // 3. Code-Review approvals of the uploader are ignored.
+ //
+ // If instead of a rebase on behalf of the uploader a normal rebase would have been done the
+ // rebaser would have been the uploader of the patch set. In this case the Code-Review
+ // approval of the rebaser would not have counted since Code-Review approvals of the uploader
+ // are ignored.
+ //
+ // In this case we assume that the change would not be submittable if a normal rebase had been
+ // done. This is not always correct (e.g. if there are approvals of multiple reviewers) but
+ // it's good enough for the metric.
+ countChangesThatWereSubmittedWithRebaserApproval.increment();
+ }
+ }
+
+ private boolean isRebaseOnBehalfOfUploader(ChangeData cd) {
+ // If the uploader differs from the real uploader the upload of the patch set has been
+ // impersonated. Impersonating the uploader is only allowed on rebase by rebasing on behalf of
+ // the uploader. Hence if the current patch set has different accounts as uploader and real
+ // uploader we can assume that it was created by rebase on behalf of the uploader.
+ return !cd.currentPatchSet().uploader().equals(cd.currentPatchSet().realUploader());
+ }
+
+ private boolean hasCodeReviewApprovalOfRealUploader(ChangeData cd) {
+ return cd.currentApprovals().stream()
+ .anyMatch(psa -> psa.accountId().equals(cd.currentPatchSet().realUploader()));
+ }
+
+ private boolean ignoresCodeReviewApprovalsOfUploader(ChangeData cd) {
+ for (SubmitRequirement submitRequirement : cd.submitRequirements().keySet()) {
+ try {
+ Predicate<ChangeData> predicate =
+ submitRequirementChangequeryBuilder
+ .get()
+ .parse(submitRequirement.submittabilityExpression().expressionString());
+ return ignoresCodeReviewApprovalsOfUploader(predicate);
+ } catch (QueryParseException e) {
+ return false;
+ }
+ }
+ return false;
+ }
+
+ private boolean ignoresCodeReviewApprovalsOfUploader(Predicate<ChangeData> predicate) {
+ if (predicate.getChildCount() == 0) {
+ // Submit requirements may require a Code-Review approval but ignore approvals by the
+ // uploader. This is done by using a label predicate with 'user=non_uploader' or
+ // 'user=non_contributor', e.g. 'label:Code-Review=+2,user=non_uploader'. After the submit
+ // requirement expression has been parsed these label predicates are represented by
+ // MagicLabelPredicate in the predicate tree. Hence to know whether Code-Review approvals of
+ // the uploader are ignored, we must check if there is any MagicLabelPredicate for the
+ // Code-Review label that ignores approvals of the uploader (aka has user set to non_uploader
+ // or non_contributor).
+ if (predicate instanceof MagicLabelPredicate) {
+ MagicLabelPredicate magicLabelPredicate = (MagicLabelPredicate) predicate;
+ if (magicLabelPredicate.getLabel().equalsIgnoreCase("Code-Review")
+ && magicLabelPredicate.ignoresUploaderApprovals()) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ for (Predicate<ChangeData> childPredicate : predicate.getChildren()) {
+ if (ignoresCodeReviewApprovalsOfUploader(childPredicate)) {
+ return true;
+ }
+ }
+ return false;
+ }
+}
diff --git a/java/com/google/gerrit/server/submit/MergeOp.java b/java/com/google/gerrit/server/submit/MergeOp.java
index 1d3ec73..d299614 100644
--- a/java/com/google/gerrit/server/submit/MergeOp.java
+++ b/java/com/google/gerrit/server/submit/MergeOp.java
@@ -246,6 +246,7 @@
private final RetryHelper retryHelper;
private final ChangeData.Factory changeDataFactory;
private final StoreSubmitRequirementsOp.Factory storeSubmitRequirementsOpFactory;
+ private final MergeMetrics mergeMetrics;
// Changes that were updated by this MergeOp.
private final Map<Change.Id, Change> updatedChanges;
@@ -280,7 +281,8 @@
TopicMetrics topicMetrics,
RetryHelper retryHelper,
ChangeData.Factory changeDataFactory,
- StoreSubmitRequirementsOp.Factory storeSubmitRequirementsOpFactory) {
+ StoreSubmitRequirementsOp.Factory storeSubmitRequirementsOpFactory,
+ MergeMetrics mergeMetrics) {
this.cmUtil = cmUtil;
this.batchUpdateFactory = batchUpdateFactory;
this.internalUserFactory = internalUserFactory;
@@ -298,6 +300,7 @@
this.changeDataFactory = changeDataFactory;
this.updatedChanges = new HashMap<>();
this.storeSubmitRequirementsOpFactory = storeSubmitRequirementsOpFactory;
+ this.mergeMetrics = mergeMetrics;
}
@Override
@@ -376,6 +379,7 @@
commitStatus.problem(cd.getId(), "Change " + cd.getId() + " is work in progress");
} else {
checkSubmitRequirements(cd);
+ mergeMetrics.countChangesThatWereSubmittedWithRebaserApproval(cd);
}
} catch (ResourceConflictException e) {
commitStatus.problem(cd.getId(), e.getMessage());
diff --git a/javatests/com/google/gerrit/acceptance/TestMetricMakerTest.java b/javatests/com/google/gerrit/acceptance/TestMetricMakerTest.java
new file mode 100644
index 0000000..3464d21
--- /dev/null
+++ b/javatests/com/google/gerrit/acceptance/TestMetricMakerTest.java
@@ -0,0 +1,202 @@
+// Copyright (C) 2023 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.acceptance;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.gerrit.metrics.Counter0;
+import com.google.gerrit.metrics.Counter1;
+import com.google.gerrit.metrics.Counter2;
+import com.google.gerrit.metrics.Counter3;
+import com.google.gerrit.metrics.Description;
+import com.google.gerrit.metrics.Field;
+import org.junit.Before;
+import org.junit.Test;
+
+/** Tests for {@link TestMetricMaker}. */
+public class TestMetricMakerTest {
+ private TestMetricMaker testMetricMaker = new TestMetricMaker();
+
+ @Before
+ public void setUp() {
+ testMetricMaker.reset();
+ }
+
+ @Test
+ public void counter0() throws Exception {
+ String counterName = "test_counter";
+ Counter0 counter = testMetricMaker.newCounter(counterName, new Description("Test Counter"));
+ assertThat(testMetricMaker.getCount(counterName)).isEqualTo(0);
+
+ counter.increment();
+ assertThat(testMetricMaker.getCount(counterName)).isEqualTo(1);
+
+ counter.incrementBy(/* value= */ 3);
+ assertThat(testMetricMaker.getCount(counterName)).isEqualTo(4);
+ }
+
+ @Test
+ public void counter1_booleanField() throws Exception {
+ String counterName = "test_counter";
+ Counter1<Boolean> counter =
+ testMetricMaker.newCounter(
+ counterName,
+ new Description("Test Counter"),
+ Field.ofBoolean("boolean_field", (metadataBuilder, booleanField) -> {}).build());
+ assertThat(testMetricMaker.getCount(counterName, true)).isEqualTo(0);
+ assertThat(testMetricMaker.getCount(counterName, false)).isEqualTo(0);
+
+ counter.increment(/* field1= */ true);
+ assertThat(testMetricMaker.getCount(counterName, true)).isEqualTo(1);
+ assertThat(testMetricMaker.getCount(counterName, false)).isEqualTo(0);
+
+ counter.incrementBy(/* field1= */ true, /* value= */ 3);
+ assertThat(testMetricMaker.getCount(counterName, true)).isEqualTo(4);
+ assertThat(testMetricMaker.getCount(counterName, false)).isEqualTo(0);
+
+ counter.increment(/* field1= */ false);
+ assertThat(testMetricMaker.getCount(counterName, true)).isEqualTo(4);
+ assertThat(testMetricMaker.getCount(counterName, false)).isEqualTo(1);
+
+ counter.incrementBy(/* field1= */ false, /* value= */ 4);
+ assertThat(testMetricMaker.getCount(counterName, true)).isEqualTo(4);
+ assertThat(testMetricMaker.getCount(counterName, false)).isEqualTo(5);
+
+ assertThat(testMetricMaker.getCount(counterName)).isEqualTo(0);
+ }
+
+ @Test
+ public void counter1_stringField() throws Exception {
+ String counterName = "test_counter";
+ Counter1<String> counter =
+ testMetricMaker.newCounter(
+ counterName,
+ new Description("Test Counter"),
+ Field.ofString("string_field", (metadataBuilder, stringField) -> {}).build());
+ assertThat(testMetricMaker.getCount(counterName, "foo")).isEqualTo(0);
+ assertThat(testMetricMaker.getCount(counterName, "bar")).isEqualTo(0);
+
+ counter.increment(/* field1= */ "foo");
+ assertThat(testMetricMaker.getCount(counterName, "foo")).isEqualTo(1);
+ assertThat(testMetricMaker.getCount(counterName, "bar")).isEqualTo(0);
+
+ counter.incrementBy(/* field1= */ "foo", /* value= */ 3);
+ assertThat(testMetricMaker.getCount(counterName, "foo")).isEqualTo(4);
+ assertThat(testMetricMaker.getCount(counterName, "bar")).isEqualTo(0);
+
+ counter.increment(/* field1= */ "bar");
+ assertThat(testMetricMaker.getCount(counterName, "foo")).isEqualTo(4);
+ assertThat(testMetricMaker.getCount(counterName, "bar")).isEqualTo(1);
+
+ counter.incrementBy(/* field1= */ "bar", /* value= */ 4);
+ assertThat(testMetricMaker.getCount(counterName, "foo")).isEqualTo(4);
+ assertThat(testMetricMaker.getCount(counterName, "bar")).isEqualTo(5);
+
+ assertThat(testMetricMaker.getCount(counterName)).isEqualTo(0);
+ }
+
+ @Test
+ public void counter2() throws Exception {
+ String counterName = "test_counter";
+ Counter2<Boolean, String> counter =
+ testMetricMaker.newCounter(
+ counterName,
+ new Description("Test Counter"),
+ Field.ofBoolean("boolean_field", (metadataBuilder, booleanField) -> {}).build(),
+ Field.ofString("string_field", (metadataBuilder, stringField) -> {}).build());
+ assertThat(testMetricMaker.getCount(counterName, true, "foo")).isEqualTo(0);
+ assertThat(testMetricMaker.getCount(counterName, false, "foo")).isEqualTo(0);
+
+ counter.increment(/* field1= */ true, /* field2= */ "foo");
+ assertThat(testMetricMaker.getCount(counterName, true, "foo")).isEqualTo(1);
+ assertThat(testMetricMaker.getCount(counterName, false, "foo")).isEqualTo(0);
+
+ counter.incrementBy(/* field1= */ true, /* field2= */ "foo", /* value= */ 3);
+ assertThat(testMetricMaker.getCount(counterName, true, "foo")).isEqualTo(4);
+ assertThat(testMetricMaker.getCount(counterName, false, "foo")).isEqualTo(0);
+
+ counter.increment(/* field1= */ false, /* field2= */ "foo");
+ assertThat(testMetricMaker.getCount(counterName, true, "foo")).isEqualTo(4);
+ assertThat(testMetricMaker.getCount(counterName, false, "foo")).isEqualTo(1);
+
+ counter.incrementBy(/* field1= */ false, /* field2= */ "foo", /* value= */ 4);
+ assertThat(testMetricMaker.getCount(counterName, true, "foo")).isEqualTo(4);
+ assertThat(testMetricMaker.getCount(counterName, false, "foo")).isEqualTo(5);
+
+ counter.increment(/* field1= */ true, /* field2= */ "bar");
+ assertThat(testMetricMaker.getCount(counterName, true, "foo")).isEqualTo(4);
+ assertThat(testMetricMaker.getCount(counterName, true, "bar")).isEqualTo(1);
+
+ counter.incrementBy(/* field1= */ true, /* field2= */ "bar", /* value= */ 5);
+ assertThat(testMetricMaker.getCount(counterName, true, "foo")).isEqualTo(4);
+ assertThat(testMetricMaker.getCount(counterName, true, "bar")).isEqualTo(6);
+
+ assertThat(testMetricMaker.getCount(counterName)).isEqualTo(0);
+ assertThat(testMetricMaker.getCount(counterName, true)).isEqualTo(0);
+ assertThat(testMetricMaker.getCount(counterName, false)).isEqualTo(0);
+ }
+
+ @Test
+ public void counter3() throws Exception {
+ String counterName = "test_counter";
+ Counter3<Boolean, String, Integer> counter =
+ testMetricMaker.newCounter(
+ counterName,
+ new Description("Test Counter"),
+ Field.ofBoolean("boolean_field", (metadataBuilder, booleanField) -> {}).build(),
+ Field.ofString("string_field", (metadataBuilder, stringField) -> {}).build(),
+ Field.ofInteger("integer_field", (metadataBuilder, stringField) -> {}).build());
+ assertThat(testMetricMaker.getCount(counterName, true, "foo", 0)).isEqualTo(0);
+ assertThat(testMetricMaker.getCount(counterName, false, "foo", 0)).isEqualTo(0);
+
+ counter.increment(/* field1= */ true, /* field2= */ "foo", /* field3= */ 0);
+ assertThat(testMetricMaker.getCount(counterName, true, "foo", 0)).isEqualTo(1);
+ assertThat(testMetricMaker.getCount(counterName, false, "foo", 0)).isEqualTo(0);
+
+ counter.incrementBy(/* field1= */ true, /* field2= */ "foo", /* field3= */ 0, /* value= */ 3);
+ assertThat(testMetricMaker.getCount(counterName, true, "foo", 0)).isEqualTo(4);
+ assertThat(testMetricMaker.getCount(counterName, false, "foo", 0)).isEqualTo(0);
+
+ counter.increment(/* field1= */ false, /* field2= */ "foo", /* field3= */ 0);
+ assertThat(testMetricMaker.getCount(counterName, true, "foo", 0)).isEqualTo(4);
+ assertThat(testMetricMaker.getCount(counterName, false, "foo", 0)).isEqualTo(1);
+
+ counter.incrementBy(/* field1= */ false, /* field2= */ "foo", /* field3= */ 0, /* value= */ 4);
+ assertThat(testMetricMaker.getCount(counterName, true, "foo", 0)).isEqualTo(4);
+ assertThat(testMetricMaker.getCount(counterName, false, "foo", 0)).isEqualTo(5);
+
+ counter.increment(/* field1= */ true, /* field2= */ "bar", /* field3= */ 0);
+ assertThat(testMetricMaker.getCount(counterName, true, "foo", 0)).isEqualTo(4);
+ assertThat(testMetricMaker.getCount(counterName, true, "bar", 0)).isEqualTo(1);
+
+ counter.incrementBy(/* field1= */ true, /* field2= */ "bar", /* field3= */ 0, /* value= */ 5);
+ assertThat(testMetricMaker.getCount(counterName, true, "foo", 0)).isEqualTo(4);
+ assertThat(testMetricMaker.getCount(counterName, true, "bar", 0)).isEqualTo(6);
+
+ counter.increment(/* field1= */ false, /* field2= */ "foo", /* field3= */ 1);
+ assertThat(testMetricMaker.getCount(counterName, true, "foo", 0)).isEqualTo(4);
+ assertThat(testMetricMaker.getCount(counterName, false, "foo", 1)).isEqualTo(1);
+
+ counter.incrementBy(/* field1= */ false, /* field2= */ "foo", /* field3= */ 1, /* value= */ 6);
+ assertThat(testMetricMaker.getCount(counterName, true, "foo", 0)).isEqualTo(4);
+ assertThat(testMetricMaker.getCount(counterName, false, "foo", 1)).isEqualTo(7);
+
+ assertThat(testMetricMaker.getCount(counterName)).isEqualTo(0);
+ assertThat(testMetricMaker.getCount(counterName, true)).isEqualTo(0);
+ assertThat(testMetricMaker.getCount(counterName, false)).isEqualTo(0);
+ assertThat(testMetricMaker.getCount(counterName, true, "foo")).isEqualTo(0);
+ assertThat(testMetricMaker.getCount(counterName, false, "foo")).isEqualTo(0);
+ }
+}
diff --git a/javatests/com/google/gerrit/acceptance/api/change/ChangeIT.java b/javatests/com/google/gerrit/acceptance/api/change/ChangeIT.java
index e3d69e1..21fc4b4 100644
--- a/javatests/com/google/gerrit/acceptance/api/change/ChangeIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/change/ChangeIT.java
@@ -101,6 +101,7 @@
import com.google.gerrit.entities.BooleanProjectConfig;
import com.google.gerrit.entities.BranchNameKey;
import com.google.gerrit.entities.Change;
+import com.google.gerrit.entities.EmailHeader.StringEmailHeader;
import com.google.gerrit.entities.LabelFunction;
import com.google.gerrit.entities.LabelId;
import com.google.gerrit.entities.LabelType;
@@ -196,6 +197,7 @@
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
+import java.util.Collections;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.Iterator;
@@ -4551,6 +4553,47 @@
.contains(String.format("%s has removed %s", admin.fullName(), reviewerInput.reviewer));
}
+ @Test
+ public void emailSubjectContainsChangeSizeBucket() throws Exception {
+ testEmailSubjectContainsChangeSizeBucket(0, "NoOp");
+ testEmailSubjectContainsChangeSizeBucket(1, "XS");
+ testEmailSubjectContainsChangeSizeBucket(9, "XS");
+ testEmailSubjectContainsChangeSizeBucket(10, "S");
+ testEmailSubjectContainsChangeSizeBucket(49, "S");
+ testEmailSubjectContainsChangeSizeBucket(50, "M");
+ testEmailSubjectContainsChangeSizeBucket(249, "M");
+ testEmailSubjectContainsChangeSizeBucket(250, "L");
+ testEmailSubjectContainsChangeSizeBucket(999, "L");
+ testEmailSubjectContainsChangeSizeBucket(1000, "XL");
+ }
+
+ private void testEmailSubjectContainsChangeSizeBucket(
+ int numberOfLines, String expectedSizeBucket) throws Exception {
+ String change;
+ if (numberOfLines == 0) {
+ // create empty change
+ ChangeInput in = new ChangeInput();
+ in.branch = Constants.MASTER;
+ in.subject = "Create a change from the API";
+ in.project = project.get();
+ ChangeInfo info = gApi.changes().create(in).get();
+ change = info.changeId;
+ } else {
+ change =
+ createChange(
+ "subject",
+ expectedSizeBucket + "-file-with-" + numberOfLines + "lines.txt",
+ Collections.nCopies(numberOfLines, "line").stream().collect(joining("\n")))
+ .getChangeId();
+ }
+ sender.clear();
+ gApi.changes().id(change).addReviewer(user.email());
+ List<Message> messages = sender.getMessages();
+ assertThat(messages).hasSize(1);
+ assertThat(((StringEmailHeader) messages.get(0).headers().get("Subject")).getString())
+ .contains("[" + expectedSizeBucket + "]");
+ }
+
private PushOneCommit.Result createWorkInProgressChange() throws Exception {
return pushTo("refs/for/master%wip");
}
diff --git a/javatests/com/google/gerrit/acceptance/api/change/RebaseIT.java b/javatests/com/google/gerrit/acceptance/api/change/RebaseIT.java
index b1fb575..3531234 100644
--- a/javatests/com/google/gerrit/acceptance/api/change/RebaseIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/change/RebaseIT.java
@@ -35,6 +35,7 @@
import com.google.gerrit.acceptance.ExtensionRegistry;
import com.google.gerrit.acceptance.PushOneCommit;
import com.google.gerrit.acceptance.TestAccount;
+import com.google.gerrit.acceptance.TestMetricMaker;
import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations;
import com.google.gerrit.common.RawInputUtil;
@@ -89,6 +90,7 @@
@Inject protected RequestScopeOperations requestScopeOperations;
@Inject protected ProjectOperations projectOperations;
@Inject protected ExtensionRegistry extensionRegistry;
+ @Inject protected TestMetricMaker testMetricMaker;
@FunctionalInterface
protected interface RebaseCall {
@@ -771,6 +773,28 @@
assertThat(gApi.changes().id(id3.get()).revision(ri3._number).related().changes).isEmpty();
}
+
+ @Test
+ public void testCountRebasesMetric() throws Exception {
+ // Create two changes both with the same parent
+ PushOneCommit.Result r = createChange();
+ testRepo.reset("HEAD~1");
+ PushOneCommit.Result r2 = createChange();
+
+ // Approve and submit the first change
+ RevisionApi revision = gApi.changes().id(r.getChangeId()).current();
+ revision.review(ReviewInput.approve());
+ revision.submit();
+
+ // Rebase the second change
+ testMetricMaker.reset();
+ rebaseCallWithInput.call(r2.getChangeId(), new RebaseInput());
+ // field1 is on_behalf_of_uploader, field2 is rebase_chain
+ assertThat(testMetricMaker.getCount("change/count_rebases", false, false)).isEqualTo(1);
+ assertThat(testMetricMaker.getCount("change/count_rebases", true, false)).isEqualTo(0);
+ assertThat(testMetricMaker.getCount("change/count_rebases", true, true)).isEqualTo(0);
+ assertThat(testMetricMaker.getCount("change/count_rebases", false, true)).isEqualTo(0);
+ }
}
public static class RebaseViaRevisionApi extends Rebase {
@@ -1125,6 +1149,36 @@
assertThat(thrown).hasMessageThat().contains("recursion not allowed");
}
+ @Test
+ public void testCountRebasesMetric() throws Exception {
+ // Create changes with the following hierarchy:
+ // * HEAD
+ // * r1
+ // * r2
+ // * r3
+ // * r4
+ PushOneCommit.Result r = createChange();
+ testRepo.reset("HEAD~1");
+ PushOneCommit.Result r2 = createChange();
+ PushOneCommit.Result r3 = createChange();
+ PushOneCommit.Result r4 = createChange();
+
+ // Approve and submit the first change
+ RevisionApi revision = gApi.changes().id(r.getChangeId()).current();
+ revision.review(ReviewInput.approve());
+ revision.submit();
+
+ // Rebase the chain.
+ testMetricMaker.reset();
+ verifyRebaseChainResponse(
+ gApi.changes().id(r4.getChangeId()).rebaseChain(), false, r2, r3, r4);
+ // field1 is on_behalf_of_uploader, field2 is rebase_chain
+ assertThat(testMetricMaker.getCount("change/count_rebases", false, true)).isEqualTo(1);
+ assertThat(testMetricMaker.getCount("change/count_rebases", false, false)).isEqualTo(0);
+ assertThat(testMetricMaker.getCount("change/count_rebases", true, true)).isEqualTo(0);
+ assertThat(testMetricMaker.getCount("change/count_rebases", true, false)).isEqualTo(0);
+ }
+
private void verifyRebaseChainResponse(
Response<RebaseChainInfo> res,
boolean shouldHaveConflicts,
diff --git a/javatests/com/google/gerrit/acceptance/api/change/RebaseOnBehalfOfUploaderIT.java b/javatests/com/google/gerrit/acceptance/api/change/RebaseOnBehalfOfUploaderIT.java
index 7030804..5a5402b 100644
--- a/javatests/com/google/gerrit/acceptance/api/change/RebaseOnBehalfOfUploaderIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/change/RebaseOnBehalfOfUploaderIT.java
@@ -27,6 +27,7 @@
import com.google.gerrit.acceptance.AbstractDaemonTest;
import com.google.gerrit.acceptance.ExtensionRegistry;
import com.google.gerrit.acceptance.ExtensionRegistry.Registration;
+import com.google.gerrit.acceptance.TestMetricMaker;
import com.google.gerrit.acceptance.UseLocalDisk;
import com.google.gerrit.acceptance.testsuite.account.AccountOperations;
import com.google.gerrit.acceptance.testsuite.change.ChangeOperations;
@@ -70,6 +71,7 @@
@Inject private ProjectOperations projectOperations;
@Inject private RequestScopeOperations requestScopeOperations;
@Inject private ExtensionRegistry extensionRegistry;
+ @Inject private TestMetricMaker testMetricMaker;
@Test
public void cannotRebaseOnBehalfOfUploaderWithAllowConflicts() throws Exception {
@@ -1021,6 +1023,95 @@
assertThat(gApi.changes().id(changeToBeRebased.get()).get().submittable).isFalse();
}
+ @Test
+ public void testSubmittedWithRebaserApprovalMetric() throws Exception {
+ // Require a Code-Review approval from a non-uploader for submit.
+ try (ProjectConfigUpdate u = updateProject(project)) {
+ u.getConfig()
+ .upsertSubmitRequirement(
+ SubmitRequirement.builder()
+ .setName(TestLabels.codeReview().getName())
+ .setSubmittabilityExpression(
+ SubmitRequirementExpression.create(
+ String.format(
+ "label:%s=MAX,user=non_uploader", TestLabels.codeReview().getName())))
+ .setAllowOverrideInChildProjects(false)
+ .build());
+ u.save();
+ }
+
+ allowPermissionToAllUsers(Permission.REBASE);
+
+ String uploaderEmail = "uploader@example.com";
+ Account.Id uploader = accountOperations.newAccount().preferredEmail(uploaderEmail).create();
+ Account.Id approver = admin.id();
+ Account.Id rebaser = accountOperations.newAccount().create();
+
+ // Create two changes both with the same parent
+ requestScopeOperations.setApiUser(uploader);
+ Change.Id changeToBeTheNewBase =
+ changeOperations.newChange().project(project).owner(uploader).create();
+ Change.Id changeToBeRebased =
+ changeOperations.newChange().project(project).owner(uploader).create();
+
+ // Approve and submit the change that will be the new base for the change that will be rebased.
+ requestScopeOperations.setApiUser(approver);
+ gApi.changes().id(changeToBeTheNewBase.get()).current().review(ReviewInput.approve());
+ testMetricMaker.reset();
+ gApi.changes().id(changeToBeTheNewBase.get()).current().submit();
+ assertThat(testMetricMaker.getCount("change/submitted_with_rebaser_approval")).isEqualTo(0);
+
+ // Rebase it on behalf of the uploader
+ requestScopeOperations.setApiUser(rebaser);
+ RebaseInput rebaseInput = new RebaseInput();
+ rebaseInput.onBehalfOfUploader = true;
+ gApi.changes().id(changeToBeRebased.get()).rebase(rebaseInput);
+
+ // Approve the change as the rebaser.
+ allowVotingOnCodeReviewToAllUsers();
+ gApi.changes().id(changeToBeRebased.get()).current().review(ReviewInput.approve());
+
+ // The change is submittable because the approval is from a user (the rebaser) that is not the
+ // uploader.
+ allowPermissionToAllUsers(Permission.SUBMIT);
+ testMetricMaker.reset();
+ gApi.changes().id(changeToBeRebased.get()).current().submit();
+ assertThat(testMetricMaker.getCount("change/submitted_with_rebaser_approval")).isEqualTo(1);
+ }
+
+ @Test
+ public void testCountRebasesMetric() throws Exception {
+ allowPermissionToAllUsers(Permission.REBASE);
+
+ Account.Id uploader = accountOperations.newAccount().create();
+ Account.Id approver = admin.id();
+ Account.Id rebaser = accountOperations.newAccount().create();
+
+ // Create two changes both with the same parent
+ requestScopeOperations.setApiUser(uploader);
+ Change.Id changeToBeTheNewBase =
+ changeOperations.newChange().project(project).owner(uploader).create();
+ Change.Id changeToBeRebased =
+ changeOperations.newChange().project(project).owner(uploader).create();
+
+ // Approve and submit the change that will be the new base for the change that will be rebased.
+ requestScopeOperations.setApiUser(approver);
+ gApi.changes().id(changeToBeTheNewBase.get()).current().review(ReviewInput.approve());
+ gApi.changes().id(changeToBeTheNewBase.get()).current().submit();
+
+ // Rebase it on behalf of the uploader
+ testMetricMaker.reset();
+ requestScopeOperations.setApiUser(rebaser);
+ RebaseInput rebaseInput = new RebaseInput();
+ rebaseInput.onBehalfOfUploader = true;
+ gApi.changes().id(changeToBeRebased.get()).rebase(rebaseInput);
+ // field1 is on_behalf_of_uploader, field2 is rebase_chain
+ assertThat(testMetricMaker.getCount("change/count_rebases", true, false)).isEqualTo(1);
+ assertThat(testMetricMaker.getCount("change/count_rebases", false, false)).isEqualTo(0);
+ assertThat(testMetricMaker.getCount("change/count_rebases", true, true)).isEqualTo(0);
+ assertThat(testMetricMaker.getCount("change/count_rebases", false, true)).isEqualTo(0);
+ }
+
private void allowPermissionToAllUsers(String permission) {
allowPermission(permission, REGISTERED_USERS);
}
diff --git a/javatests/com/google/gerrit/acceptance/rest/change/DeleteVoteIT.java b/javatests/com/google/gerrit/acceptance/rest/change/DeleteVoteIT.java
index 016b1e6..6491202 100644
--- a/javatests/com/google/gerrit/acceptance/rest/change/DeleteVoteIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/change/DeleteVoteIT.java
@@ -140,6 +140,33 @@
verifyCannotDeleteVote(true);
}
+ @Test
+ public void deleteAlreadyDeletedVote_returnsNotFoundAndWithoutEmails() throws Exception {
+ projectOperations
+ .project(project)
+ .forUpdate()
+ .add(allow(Permission.REMOVE_REVIEWER).ref("refs/*").group(REGISTERED_USERS))
+ .update();
+ PushOneCommit.Result r = createChange();
+ gApi.changes().id(r.getChangeId()).revision(r.getCommit().name()).review(ReviewInput.approve());
+ String deleteAdminVoteEndPoint =
+ "/changes/"
+ + r.getChangeId()
+ + "/reviewers/"
+ + admin.id().toString()
+ + "/votes/Code-Review";
+
+ sender.clear();
+ RestResponse response = userRestSession.delete(deleteAdminVoteEndPoint);
+ response.assertNoContent();
+ assertThat(sender.getMessages()).hasSize(1);
+
+ sender.clear();
+ response = userRestSession.delete(deleteAdminVoteEndPoint);
+ response.assertNotFound();
+ assertThat(sender.getMessages()).isEmpty();
+ }
+
private void verifyDeleteVote(boolean onRevisionLevel) throws Exception {
PushOneCommit.Result r = createChange();
gApi.changes().id(r.getChangeId()).revision(r.getCommit().name()).review(ReviewInput.approve());
diff --git a/javatests/com/google/gerrit/acceptance/server/mail/ChangeNotificationsIT.java b/javatests/com/google/gerrit/acceptance/server/mail/ChangeNotificationsIT.java
index e44bfcf..cced47f 100644
--- a/javatests/com/google/gerrit/acceptance/server/mail/ChangeNotificationsIT.java
+++ b/javatests/com/google/gerrit/acceptance/server/mail/ChangeNotificationsIT.java
@@ -984,7 +984,7 @@
StagedPreChange spc = stagePreChange("refs/for/master");
assertThat(sender)
.sent("newchange", spc)
- .title(String.format("[S] Change in %s[master]: test commit", project));
+ .title(String.format("[XS] Change in %s[master]: test commit", project));
assertThat(sender).didNotSend();
}
diff --git a/modules/jgit b/modules/jgit
index 66b871b..596c445 160000
--- a/modules/jgit
+++ b/modules/jgit
@@ -1 +1 @@
-Subproject commit 66b871b777c1a58337e80dd03db68bb76b145a93
+Subproject commit 596c445af22ed9b6e0b7e35de44c127fcb8ecf7d
diff --git a/polygerrit-ui/app/api/diff.ts b/polygerrit-ui/app/api/diff.ts
index e3b3ad4..19e9d99 100644
--- a/polygerrit-ui/app/api/diff.ts
+++ b/polygerrit-ui/app/api/diff.ts
@@ -336,6 +336,12 @@
lineNum: LineNumber;
}
+export declare interface LineSelectedEventDetail {
+ number: LineNumber;
+ side: Side;
+ path?: string;
+}
+
// TODO: Currently unused and not fired.
export declare interface RenderProgressEventDetail {
linesRendered: number;
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 ce0ea02..4d0d3f1 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
@@ -24,7 +24,7 @@
LabelNameToLabelTypeInfoMap,
RepoName,
} from '../../../types/common';
-import {fire, fireEvent} from '../../../utils/event-util';
+import {fire} from '../../../utils/event-util';
import {IronInputElement} from '@polymer/iron-input/iron-input';
import {fontStyles} from '../../../styles/gr-font-styles';
import {formStyles} from '../../../styles/gr-form-styles';
@@ -34,18 +34,6 @@
import {BindValueChangeEvent, ValueChangedEvent} from '../../../types/events';
import {assertIsDefined, queryAndAssert} from '../../../utils/common-util';
-/**
- * Fired when the section has been modified or removed.
- *
- * @event access-modified
- */
-
-/**
- * Fired when a section that was previously added was removed.
- *
- * @event added-section-removed
- */
-
const GLOBAL_NAME = 'GLOBAL_CAPABILITIES';
// The name that gets automatically input when a new reference is added.
@@ -300,7 +288,7 @@
// For a new section, this is not fired because new permissions and
// rules have to be added in order to save, modifying the ref is not
// enough.
- fireEvent(this, 'access-modified');
+ fire(this, 'access-modified', {});
}
this.section.value.updatedId = this.section.id;
this.requestUpdate();
@@ -432,11 +420,11 @@
return;
}
if (this.section.value.added) {
- fireEvent(this, 'added-section-removed');
+ fire(this, 'added-section-removed', {});
}
this.deleted = true;
this.section.value.deleted = true;
- fireEvent(this, 'access-modified');
+ fire(this, 'access-modified', {});
}
_handleUndoRemove() {
@@ -533,6 +521,10 @@
declare global {
interface HTMLElementEventMap {
+ /** Fired when the section has been modified or removed. */
+ 'access-modified': CustomEvent<{}>;
+ /** Fired when a section that was previously added was removed. */
+ 'added-section-removed': CustomEvent<{}>;
'section-changed': ValueChangedEvent<PermissionAccessSection>;
}
interface HTMLElementTagNameMap {
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 42ec988..61fe0c4 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
@@ -7,6 +7,7 @@
import {sharedStyles} from '../../../styles/shared-styles';
import {css, html, LitElement} from 'lit';
import {customElement, property} from 'lit/decorators.js';
+import {fireNoBubble} from '../../../utils/event-util';
declare global {
interface HTMLElementTagNameMap {
@@ -68,22 +69,12 @@
_handleConfirmTap(e: Event) {
e.preventDefault();
e.stopPropagation();
- this.dispatchEvent(
- new CustomEvent('confirm', {
- composed: true,
- bubbles: false,
- })
- );
+ fireNoBubble(this, 'confirm', {});
}
_handleCancelTap(e: Event) {
e.preventDefault();
e.stopPropagation();
- this.dispatchEvent(
- new CustomEvent('cancel', {
- composed: true,
- bubbles: false,
- })
- );
+ fireNoBubble(this, 'cancel', {});
}
}
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 3254b5c..b3f1e96 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
@@ -23,8 +23,8 @@
import {sharedStyles} from '../../../styles/shared-styles';
import {LitElement, PropertyValues, css, html} from 'lit';
import {customElement, property, query, state} from 'lit/decorators.js';
-import {BindValueChangeEvent} from '../../../types/events';
-import {fireEvent} from '../../../utils/event-util';
+import {BindValueChangeEvent, ValueChangedEvent} from '../../../types/events';
+import {fire} from '../../../utils/event-util';
import {subscribe} from '../../lit/subscription-controller';
import {configModelToken} from '../../../models/config/config-model';
import {resolve} from '../../../models/dependency';
@@ -38,6 +38,9 @@
interface HTMLElementTagNameMap {
'gr-create-change-dialog': GrCreateChangeDialog;
}
+ interface HTMLElementEventMap {
+ 'can-create-change': CustomEvent<{}>;
+ }
}
@customElement('gr-create-change-dialog')
@@ -125,7 +128,7 @@
.text=${this.branch}
.query=${this.query}
placeholder="Destination branch"
- @text-changed=${(e: CustomEvent) => {
+ @text-changed=${(e: ValueChangedEvent<BranchName>) => {
this.branch = e.detail.value;
}}
>
@@ -207,7 +210,7 @@
}
private allowCreate() {
- fireEvent(this, 'can-create-change');
+ fire(this, 'can-create-change', {});
}
handleCreateChange(): Promise<void> {
diff --git a/polygerrit-ui/app/elements/admin/gr-create-group-dialog/gr-create-group-dialog.ts b/polygerrit-ui/app/elements/admin/gr-create-group-dialog/gr-create-group-dialog.ts
index 96688e9..893343f 100644
--- a/polygerrit-ui/app/elements/admin/gr-create-group-dialog/gr-create-group-dialog.ts
+++ b/polygerrit-ui/app/elements/admin/gr-create-group-dialog/gr-create-group-dialog.ts
@@ -13,7 +13,7 @@
import {LitElement, PropertyValues, css, html} from 'lit';
import {customElement, query, property} from 'lit/decorators.js';
import {BindValueChangeEvent} from '../../../types/events';
-import {fireEvent} from '../../../utils/event-util';
+import {fire} from '../../../utils/event-util';
import {createGroupUrl} from '../../../models/views/group';
import {resolve} from '../../../models/dependency';
import {navigationToken} from '../../core/gr-navigation/gr-navigation';
@@ -22,6 +22,9 @@
interface HTMLElementTagNameMap {
'gr-create-group-dialog': GrCreateGroupDialog;
}
+ interface HTMLElementEventMap {
+ 'has-new-group-name': CustomEvent<{}>;
+ }
}
@customElement('gr-create-group-dialog')
@@ -75,7 +78,7 @@
}
private updateGroupName() {
- fireEvent(this, 'has-new-group-name');
+ fire(this, 'has-new-group-name', {});
}
override focus() {
diff --git a/polygerrit-ui/app/elements/admin/gr-create-pointer-dialog/gr-create-pointer-dialog.ts b/polygerrit-ui/app/elements/admin/gr-create-pointer-dialog/gr-create-pointer-dialog.ts
index 558d571..12f36ec 100644
--- a/polygerrit-ui/app/elements/admin/gr-create-pointer-dialog/gr-create-pointer-dialog.ts
+++ b/polygerrit-ui/app/elements/admin/gr-create-pointer-dialog/gr-create-pointer-dialog.ts
@@ -13,13 +13,16 @@
import {LitElement, PropertyValues, css, html} from 'lit';
import {customElement, property, state} from 'lit/decorators.js';
import {BindValueChangeEvent} from '../../../types/events';
-import {fireAlert, fireEvent, fireReload} from '../../../utils/event-util';
+import {fireAlert, fire, fireReload} from '../../../utils/event-util';
import {RepoDetailView} from '../../../models/views/repo';
declare global {
interface HTMLElementTagNameMap {
'gr-create-pointer-dialog': GrCreatePointerDialog;
}
+ interface HTMLElementEventMap {
+ 'update-item-name': CustomEvent<{}>;
+ }
}
@customElement('gr-create-pointer-dialog')
@@ -113,7 +116,7 @@
}
private updateItemName() {
- fireEvent(this, 'update-item-name');
+ fire(this, 'update-item-name', {});
}
handleCreateItem() {
diff --git a/polygerrit-ui/app/elements/admin/gr-create-repo-dialog/gr-create-repo-dialog.ts b/polygerrit-ui/app/elements/admin/gr-create-repo-dialog/gr-create-repo-dialog.ts
index ed57830..1a70f2b 100644
--- a/polygerrit-ui/app/elements/admin/gr-create-repo-dialog/gr-create-repo-dialog.ts
+++ b/polygerrit-ui/app/elements/admin/gr-create-repo-dialog/gr-create-repo-dialog.ts
@@ -20,26 +20,25 @@
import {sharedStyles} from '../../../styles/shared-styles';
import {LitElement, css, html} from 'lit';
import {customElement, query, property, state} from 'lit/decorators.js';
-import {fireEvent} from '../../../utils/event-util';
+import {fire} from '../../../utils/event-util';
import {throwingErrorCallback} from '../../shared/gr-rest-api-interface/gr-rest-apis/gr-rest-api-helper';
import {createRepoUrl} from '../../../models/views/repo';
import {resolve} from '../../../models/dependency';
import {navigationToken} from '../../core/gr-navigation/gr-navigation';
+import {ValueChangedEvent} from '../../../types/events';
declare global {
interface HTMLElementTagNameMap {
'gr-create-repo-dialog': GrCreateRepoDialog;
}
+ interface HTMLElementEventMap {
+ /** Fired when repostiory name is entered. */
+ 'new-repo-name': CustomEvent<{}>;
+ }
}
@customElement('gr-create-repo-dialog')
export class GrCreateRepoDialog extends LitElement {
- /**
- * Fired when repostiory name is entered.
- *
- * @event new-repo-name
- */
-
@query('input')
input?: HTMLInputElement;
@@ -232,39 +231,41 @@
return groups;
}
- private handleRightsTextChanged(e: CustomEvent) {
+ private handleRightsTextChanged(e: ValueChangedEvent) {
this.repoConfig.parent = e.detail.value as RepoName;
this.requestUpdate();
}
- private handleOwnerTextChanged(e: CustomEvent) {
+ private handleOwnerTextChanged(e: ValueChangedEvent) {
this.repoOwner = e.detail.value;
}
- private handleOwnerValueChanged(e: CustomEvent) {
+ private handleOwnerValueChanged(e: ValueChangedEvent) {
this.repoOwnerId = e.detail.value as GroupId;
}
- private handleNameBindValueChanged(e: CustomEvent) {
+ private handleNameBindValueChanged(e: ValueChangedEvent) {
this.repoConfig.name = e.detail.value as RepoName;
// nameChanged needs to be set before the event is fired,
// because when the event is fired, gr-repo-list gets
// the nameChanged value.
this.nameChanged = !!e.detail.value;
- fireEvent(this, 'new-repo-name');
+ fire(this, 'new-repo-name', {});
this.requestUpdate();
}
- private handleBranchNameBindValueChanged(e: CustomEvent) {
+ private handleBranchNameBindValueChanged(e: ValueChangedEvent) {
this.defaultBranch = e.detail.value as BranchName;
}
- private handleCreateEmptyCommitBindValueChanged(e: CustomEvent) {
+ private handleCreateEmptyCommitBindValueChanged(
+ e: ValueChangedEvent<boolean>
+ ) {
this.repoConfig.create_empty_commit = e.detail.value;
this.requestUpdate();
}
- private handlePermissionsOnlyBindValueChanged(e: CustomEvent) {
+ private handlePermissionsOnlyBindValueChanged(e: ValueChangedEvent<boolean>) {
this.repoConfig.permissions_only = e.detail.value;
this.requestUpdate();
}
diff --git a/polygerrit-ui/app/elements/admin/gr-group-members/gr-group-members.ts b/polygerrit-ui/app/elements/admin/gr-group-members/gr-group-members.ts
index af9977b..2fcec51 100644
--- a/polygerrit-ui/app/elements/admin/gr-group-members/gr-group-members.ts
+++ b/polygerrit-ui/app/elements/admin/gr-group-members/gr-group-members.ts
@@ -43,6 +43,7 @@
import {resolve} from '../../../models/dependency';
import {modalStyles} from '../../../styles/gr-modal-styles';
import {throwingErrorCallback} from '../../shared/gr-rest-api-interface/gr-rest-apis/gr-rest-api-helper';
+import {ValueChangedEvent} from '../../../types/events';
const SAVING_ERROR_TEXT =
'Group may not exist, or you may not have ' + 'permission to add it';
@@ -546,22 +547,22 @@
});
}
- private handleGroupMemberTextChanged(e: CustomEvent) {
+ private handleGroupMemberTextChanged(e: ValueChangedEvent) {
if (this.loading) return;
this.groupMemberSearchName = e.detail.value;
}
- private handleGroupMemberValueChanged(e: CustomEvent) {
+ private handleGroupMemberValueChanged(e: ValueChangedEvent<number>) {
if (this.loading) return;
this.groupMemberSearchId = e.detail.value;
}
- private handleIncludedGroupTextChanged(e: CustomEvent) {
+ private handleIncludedGroupTextChanged(e: ValueChangedEvent) {
if (this.loading) return;
this.includedGroupSearchName = e.detail.value;
}
- private handleIncludedGroupValueChanged(e: CustomEvent) {
+ private handleIncludedGroupValueChanged(e: ValueChangedEvent) {
if (this.loading) return;
this.includedGroupSearchId = e.detail.value;
}
diff --git a/polygerrit-ui/app/elements/admin/gr-group-members/gr-group-members_test.ts b/polygerrit-ui/app/elements/admin/gr-group-members/gr-group-members_test.ts
index 0841595..30085e3 100644
--- a/polygerrit-ui/app/elements/admin/gr-group-members/gr-group-members_test.ts
+++ b/polygerrit-ui/app/elements/admin/gr-group-members/gr-group-members_test.ts
@@ -25,7 +25,7 @@
} from '../../../types/common';
import {GrButton} from '../../shared/gr-button/gr-button';
import {GrAutocomplete} from '../../shared/gr-autocomplete/gr-autocomplete';
-import {EventType, PageErrorEvent} from '../../../types/events';
+import {PageErrorEvent} from '../../../types/events';
import {getAccountSuggestions} from '../../../utils/account-util';
import {getAppContext} from '../../../services/app-context';
import {fixture, html, assert} from '@open-wc/testing';
@@ -446,7 +446,7 @@
const memberName = 'bad-name';
const alertStub = sinon.stub();
- element.addEventListener(EventType.SHOW_ALERT, alertStub);
+ element.addEventListener('show-alert', alertStub);
const errorResponse = {...new Response(), status: 404, ok: false};
stubRestApi('saveIncludedGroup').callsFake((_, _non, errFn) => {
if (errFn !== undefined) {
diff --git a/polygerrit-ui/app/elements/admin/gr-group/gr-group.ts b/polygerrit-ui/app/elements/admin/gr-group/gr-group.ts
index ba2b3fd..1ec83efa8 100644
--- a/polygerrit-ui/app/elements/admin/gr-group/gr-group.ts
+++ b/polygerrit-ui/app/elements/admin/gr-group/gr-group.ts
@@ -13,11 +13,11 @@
AutocompleteQuery,
} from '../../shared/gr-autocomplete/gr-autocomplete';
import {GroupId, GroupInfo, GroupName} from '../../../types/common';
-import {firePageError, fireTitleChange} from '../../../utils/event-util';
+import {fire, firePageError, fireTitleChange} from '../../../utils/event-util';
import {getAppContext} from '../../../services/app-context';
import {ErrorCallback} from '../../../api/rest';
import {convertToString} from '../../../utils/string-util';
-import {BindValueChangeEvent} from '../../../types/events';
+import {BindValueChangeEvent, ValueChangedEvent} from '../../../types/events';
import {fontStyles} from '../../../styles/gr-font-styles';
import {formStyles} from '../../../styles/gr-form-styles';
import {sharedStyles} from '../../../styles/shared-styles';
@@ -48,16 +48,13 @@
interface HTMLElementTagNameMap {
'gr-group': GrGroup;
}
+ interface HTMLElementEventMap {
+ 'name-changed': CustomEvent<GroupNameChangedDetail>;
+ }
}
@customElement('gr-group')
export class GrGroup extends LitElement {
- /**
- * Fired when the group name changes.
- *
- * @event name-changed
- */
-
private readonly query: AutocompleteQuery;
@property({type: String})
@@ -373,13 +370,7 @@
name: groupName,
external: !this.groupIsInternal,
};
- this.dispatchEvent(
- new CustomEvent('name-changed', {
- detail,
- composed: true,
- bubbles: true,
- })
- );
+ fire(this, 'name-changed', detail);
this.requestUpdate();
}
@@ -455,25 +446,25 @@
return id.match(INTERNAL_GROUP_REGEX) ? id : decodeURIComponent(id);
}
- private handleNameTextChanged(e: CustomEvent) {
+ private handleNameTextChanged(e: ValueChangedEvent) {
if (!this.groupConfig || this.loading) return;
this.groupConfig.name = e.detail.value as GroupName;
this.requestUpdate();
}
- private handleOwnerTextChanged(e: CustomEvent) {
+ private handleOwnerTextChanged(e: ValueChangedEvent) {
if (!this.groupConfig || this.loading) return;
this.groupConfig.owner = e.detail.value;
this.requestUpdate();
}
- private handleOwnerValueChanged(e: CustomEvent) {
+ private handleOwnerValueChanged(e: ValueChangedEvent) {
if (this.loading) return;
this.groupConfigOwner = e.detail.value;
this.requestUpdate();
}
- private handleDescriptionTextChanged(e: CustomEvent) {
+ private handleDescriptionTextChanged(e: ValueChangedEvent) {
if (!this.groupConfig || this.loading) return;
this.groupConfig.description = e.detail.value;
this.requestUpdate();
diff --git a/polygerrit-ui/app/elements/admin/gr-permission/gr-permission.ts b/polygerrit-ui/app/elements/admin/gr-permission/gr-permission.ts
index 07b7c87..38123ad 100644
--- a/polygerrit-ui/app/elements/admin/gr-permission/gr-permission.ts
+++ b/polygerrit-ui/app/elements/admin/gr-permission/gr-permission.ts
@@ -34,7 +34,7 @@
EditableRepoAccessGroups,
} from '../gr-repo-access/gr-repo-access-interfaces';
import {getAppContext} from '../../../services/app-context';
-import {fire, fireEvent} from '../../../utils/event-util';
+import {fire} from '../../../utils/event-util';
import {sharedStyles} from '../../../styles/shared-styles';
import {paperStyles} from '../../../styles/gr-paper-styles';
import {formStyles} from '../../../styles/gr-form-styles';
@@ -64,16 +64,6 @@
value: GroupInfo;
}
-/**
- * Fired when the permission has been modified or removed.
- *
- * @event access-modified
- */
-/**
- * Fired when a permission that was previously added was removed.
- *
- * @event added-permission-removed
- */
@customElement('gr-permission')
export class GrPermission extends LitElement {
@property({type: String})
@@ -361,7 +351,7 @@
this.permission.value.modified = true;
this.permission.value.exclusive = (e.target as HTMLInputElement).checked;
// Allows overall access page to know a change has been made.
- fireEvent(this, 'access-modified');
+ fire(this, 'access-modified', {});
}
handleRemovePermission() {
@@ -369,11 +359,11 @@
return;
}
if (this.permission.value.added) {
- fireEvent(this, 'added-permission-removed');
+ fire(this, 'added-permission-removed', {});
}
this.deleted = true;
this.permission.value.deleted = true;
- fireEvent(this, 'access-modified');
+ fire(this, 'access-modified', {});
}
private handleRulesChanged() {
@@ -542,7 +532,7 @@
const value = this.rules[this.rules.length - 1].value;
value!.added = true;
this.permission.value.rules[groupId] = value!;
- fireEvent(this, 'access-modified');
+ fire(this, 'access-modified', {});
this.requestUpdate();
}
@@ -565,6 +555,11 @@
e.preventDefault();
}
+ // TODO: Do not use generic `CustomEvent`.
+ // There is something fishy going on here though.
+ // `e.detail.value` is of type `Rule`, but `splice()` expects a `number`.
+ // Did not look closer, but this seems to be broken. Should `e.detail.value`
+ // be replaced by `1` maybe??
private handleRuleChanged(e: CustomEvent, index: number) {
this.rules!.splice(index, e.detail.value);
this.handleRulesChanged();
@@ -574,6 +569,8 @@
declare global {
interface HTMLElementEventMap {
+ /** Fired when a permission that was previously added was removed. */
+ 'added-permission-removed': CustomEvent<{}>;
'permission-changed': ValueChangedEvent<
PermissionArrayItem<EditablePermissionInfo>
>;
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 54a83ee..5afaa9d 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
@@ -15,21 +15,19 @@
import {LitElement, html, css} from 'lit';
import {customElement, property, state} from 'lit/decorators.js';
import {BindValueChangeEvent} from '../../../types/events';
+import {fireNoBubbleNoCompose} from '../../../utils/event-util';
declare global {
interface HTMLElementTagNameMap {
'gr-plugin-config-array-editor': GrPluginConfigArrayEditor;
}
+ interface HTMLElementEventMap {
+ 'plugin-config-option-changed': CustomEvent<PluginConfigOptionsChangedEventDetail>;
+ }
}
@customElement('gr-plugin-config-array-editor')
export class GrPluginConfigArrayEditor extends LitElement {
- /**
- * Fired when the plugin config option changes.
- *
- * @event plugin-config-option-changed
- */
-
// private but used in test
@state() newValue = '';
@@ -175,9 +173,7 @@
info: {...info, values},
notifyPath: `${_key}.values`,
};
- this.dispatchEvent(
- new CustomEvent('plugin-config-option-changed', {detail})
- );
+ fireNoBubbleNoCompose(this, 'plugin-config-option-changed', detail);
}
private handleBindValueChangedNewValue(e: BindValueChangeEvent) {
diff --git a/polygerrit-ui/app/elements/admin/gr-repo-access/gr-repo-access.ts b/polygerrit-ui/app/elements/admin/gr-repo-access/gr-repo-access.ts
index 52e0b3f..d00fa40 100644
--- a/polygerrit-ui/app/elements/admin/gr-repo-access/gr-repo-access.ts
+++ b/polygerrit-ui/app/elements/admin/gr-repo-access/gr-repo-access.ts
@@ -20,6 +20,7 @@
import {GrButton} from '../../shared/gr-button/gr-button';
import {GrAccessSection} from '../gr-access-section/gr-access-section';
import {
+ AutocompleteCommitEvent,
AutocompleteQuery,
AutocompleteSuggestion,
} from '../../shared/gr-autocomplete/gr-autocomplete';
@@ -194,7 +195,7 @@
id="editInheritFromInput"
.text=${this.inheritFromFilter}
.query=${this.query}
- @commit=${(e: ValueChangedEvent) => {
+ @commit=${(e: AutocompleteCommitEvent) => {
this.handleUpdateInheritFrom(e);
}}
@bind-value-changed=${(e: ValueChangedEvent) => {
@@ -388,7 +389,7 @@
}
// private but used in test
- handleUpdateInheritFrom(e: ValueChangedEvent) {
+ handleUpdateInheritFrom(e: AutocompleteCommitEvent) {
this.inheritsFrom = {
...(this.inheritsFrom ?? {}),
id: e.detail.value as UrlEncodedRepoName,
diff --git a/polygerrit-ui/app/elements/admin/gr-repo-commands/gr-repo-commands_test.ts b/polygerrit-ui/app/elements/admin/gr-repo-commands/gr-repo-commands_test.ts
index dab2706..af2831a 100644
--- a/polygerrit-ui/app/elements/admin/gr-repo-commands/gr-repo-commands_test.ts
+++ b/polygerrit-ui/app/elements/admin/gr-repo-commands/gr-repo-commands_test.ts
@@ -13,7 +13,7 @@
stubRestApi,
} from '../../../test/test-utils';
import {GrDialog} from '../../shared/gr-dialog/gr-dialog';
-import {EventType, PageErrorEvent} from '../../../types/events';
+import {PageErrorEvent} from '../../../types/events';
import {RepoName} from '../../../types/common';
import {GrButton} from '../../shared/gr-button/gr-button';
import {fixture, html, assert} from '@open-wc/testing';
@@ -147,7 +147,7 @@
handleSpy = sinon.spy(element, 'handleEditRepoConfig');
alertStub = sinon.stub();
element.repo = 'test' as RepoName;
- element.addEventListener(EventType.SHOW_ALERT, alertStub);
+ element.addEventListener('show-alert', alertStub);
});
test('successful creation of change', async () => {
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 f8dcd32..2772519 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
@@ -25,8 +25,7 @@
PluginOption,
} from './gr-repo-plugin-config-types';
import {paperStyles} from '../../../styles/gr-paper-styles';
-
-const PLUGIN_CONFIG_CHANGED_EVENT_NAME = 'plugin-config-changed';
+import {fire} from '../../../utils/event-util';
export interface ConfigChangeInfo {
_key: string; // parameterName of PluginParameterToConfigParameterInfoMap
@@ -255,14 +254,7 @@
name,
config: {...config, [_key]: info},
};
-
- this.dispatchEvent(
- new CustomEvent(PLUGIN_CONFIG_CHANGED_EVENT_NAME, {
- detail,
- bubbles: true,
- composed: true,
- })
- );
+ fire(this, 'plugin-config-changed', detail);
}
/**
@@ -277,4 +269,7 @@
interface HTMLElementTagNameMap {
'gr-repo-plugin-config': GrRepoPluginConfig;
}
+ interface HTMLElementEventMap {
+ 'plugin-config-changed': CustomEvent<PluginConfigChangeDetail>;
+ }
}
diff --git a/polygerrit-ui/app/elements/admin/gr-rule-editor/gr-rule-editor.ts b/polygerrit-ui/app/elements/admin/gr-rule-editor/gr-rule-editor.ts
index 5f0a171..4e41dfe 100644
--- a/polygerrit-ui/app/elements/admin/gr-rule-editor/gr-rule-editor.ts
+++ b/polygerrit-ui/app/elements/admin/gr-rule-editor/gr-rule-editor.ts
@@ -8,28 +8,16 @@
import '../../shared/gr-select/gr-select';
import {encodeURL, getBaseUrl} from '../../../utils/url-util';
import {AccessPermissionId} from '../../../utils/access-util';
-import {fireEvent} from '../../../utils/event-util';
+import {fire} from '../../../utils/event-util';
import {formStyles} from '../../../styles/gr-form-styles';
import {sharedStyles} from '../../../styles/shared-styles';
import {LitElement, PropertyValues, html, css} from 'lit';
import {customElement, property, state} from 'lit/decorators.js';
-import {BindValueChangeEvent} from '../../../types/events';
+import {BindValueChangeEvent, ValueChangedEvent} from '../../../types/events';
import {ifDefined} from 'lit/directives/if-defined.js';
import {EditablePermissionRuleInfo} from '../gr-repo-access/gr-repo-access-interfaces';
import {PermissionAction} from '../../../constants/constants';
-/**
- * Fired when the rule has been modified or removed.
- *
- * @event access-modified
- */
-
-/**
- * Fired when a rule that was previously added was removed.
- *
- * @event added-rule-removed
- */
-
const PRIORITY_OPTIONS = [PermissionAction.BATCH, PermissionAction.INTERACTIVE];
const Action = {
@@ -81,6 +69,11 @@
interface HTMLElementTagNameMap {
'gr-rule-editor': GrRuleEditor;
}
+ interface HTMLElementEventMap {
+ /** Fired when a rule that was previously added was removed. */
+ 'added-rule-removed': CustomEvent<{}>;
+ 'rule-changed': ValueChangedEvent<Rule | undefined>;
+ }
}
@customElement('gr-rule-editor')
@@ -431,14 +424,14 @@
private handleRemoveRule() {
if (!this.rule?.value) return;
if (this.rule.value.added) {
- fireEvent(this, 'added-rule-removed');
+ fire(this, 'added-rule-removed', {});
}
this.deleted = true;
this.rule.value.deleted = true;
this.handleRuleChange();
- fireEvent(this, 'access-modified');
+ fire(this, 'access-modified', {});
}
private handleUndoRemove() {
@@ -476,7 +469,7 @@
this.handleRuleChange();
// Allows overall access page to know a change has been made.
- fireEvent(this, 'access-modified');
+ fire(this, 'access-modified', {});
}
// private but used in test
@@ -537,13 +530,6 @@
private handleRuleChange() {
this.requestUpdate('rule');
-
- this.dispatchEvent(
- new CustomEvent('rule-changed', {
- detail: {value: this.rule},
- composed: true,
- bubbles: true,
- })
- );
+ fire(this, 'rule-changed', {value: this.rule});
}
}
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list-bulk-abandon-flow/gr-change-list-bulk-abandon-flow.ts b/polygerrit-ui/app/elements/change-list/gr-change-list-bulk-abandon-flow/gr-change-list-bulk-abandon-flow.ts
index 11de37e..df63780 100644
--- a/polygerrit-ui/app/elements/change-list/gr-change-list-bulk-abandon-flow/gr-change-list-bulk-abandon-flow.ts
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list-bulk-abandon-flow/gr-change-list-bulk-abandon-flow.ts
@@ -56,7 +56,7 @@
<dialog id="actionModal" tabindex="-1">
<gr-dialog
.disableCancel=${!this.isCancelEnabled()}
- .disabled=${!this.isConfirmEnabled()}
+ .disabled=${this.isDisabled()}
@confirm=${() => this.handleConfirm()}
@cancel=${() => this.handleClose()}
.cancelLabel=${'Close'}
@@ -104,13 +104,13 @@
);
}
- private isConfirmEnabled() {
+ private isDisabled() {
// Action is allowed if none of the changes have any bulk action performed
// on them. In case an error happens then we keep the button disabled.
for (const status of this.progress.values()) {
- if (status !== ProgressStatus.NOT_STARTED) return false;
+ if (status !== ProgressStatus.NOT_STARTED) return true;
}
- return true;
+ return false;
}
private isCancelEnabled() {
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list-bulk-vote-flow/gr-change-list-bulk-vote-flow.ts b/polygerrit-ui/app/elements/change-list/gr-change-list-bulk-vote-flow/gr-change-list-bulk-vote-flow.ts
index a9b8028..db82523 100644
--- a/polygerrit-ui/app/elements/change-list/gr-change-list-bulk-vote-flow/gr-change-list-bulk-vote-flow.ts
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list-bulk-vote-flow/gr-change-list-bulk-vote-flow.ts
@@ -161,7 +161,9 @@
<dialog id="actionModal" tabindex="-1">
<gr-dialog
.disableCancel=${!this.isCancelEnabled()}
- .disabled=${!this.isConfirmEnabled()}
+ .disabled=${this.isDisabled(
+ triggerLabels.length + nonTriggerLabels.length
+ )}
?loading=${this.isLoading()}
.loadingLabel=${'Voting in progress...'}
@confirm=${() => this.handleConfirm()}
@@ -289,11 +291,12 @@
return getOverallStatus(this.progressByChange) === ProgressStatus.RUNNING;
}
- private isConfirmEnabled() {
+ private isDisabled(permittedLabelsCount: number) {
// Action is allowed if none of the changes have any bulk action performed
// on them. In case an error happens then we keep the button disabled.
- return (
- getOverallStatus(this.progressByChange) === ProgressStatus.NOT_STARTED
+ return !(
+ getOverallStatus(this.progressByChange) === ProgressStatus.NOT_STARTED &&
+ permittedLabelsCount > 0
);
}
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list-bulk-vote-flow/gr-change-list-bulk-vote-flow_test.ts b/polygerrit-ui/app/elements/change-list/gr-change-list-bulk-vote-flow/gr-change-list-bulk-vote-flow_test.ts
index 654ed91..8a5bf47 100644
--- a/polygerrit-ui/app/elements/change-list/gr-change-list-bulk-vote-flow/gr-change-list-bulk-vote-flow_test.ts
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list-bulk-vote-flow/gr-change-list-bulk-vote-flow_test.ts
@@ -307,17 +307,18 @@
);
// No common label with change1 so button is disabled
- change2.labels = {
+ const c2 = {...change2}; // create copy so other tests are not affected
+ c2.labels = {
x: {value: null} as LabelInfo,
y: {value: null} as LabelInfo,
z: {value: null} as LabelInfo,
};
- change2.submit_requirements = [
+ c2.submit_requirements = [
createSubmitRequirementResultInfo('label:x=MAX'),
createSubmitRequirementResultInfo('label:y=MAX'),
createSubmitRequirementResultInfo('label:z=MAX'),
];
- changes.push({...change2});
+ changes.push({...c2});
getChangesStub.restore();
getChangesStub.returns(Promise.resolve(changes));
model.sync(changes);
@@ -484,6 +485,45 @@
assert.equal(dispatchEventStub.lastCall.args[0].type, 'reload');
});
+ test('button is disabled if no votes are possible', async () => {
+ const c2 = {...change2}; // create copy so other tests are not affected
+ c2.labels = {
+ x: {value: null} as LabelInfo,
+ y: {value: null} as LabelInfo,
+ z: {value: null} as LabelInfo,
+ };
+ c2.submit_requirements = [
+ createSubmitRequirementResultInfo('label:x=MAX'),
+ createSubmitRequirementResultInfo('label:y=MAX'),
+ createSubmitRequirementResultInfo('label:z=MAX'),
+ ];
+
+ const changes: ChangeInfo[] = [change1, c2];
+ getChangesStub.returns(Promise.resolve(changes));
+
+ stubRestApi('saveChangeReview').callsFake(
+ (_changeNum, _patchNum, _review, errFn) =>
+ Promise.resolve(new Response()).then(res => {
+ errFn && errFn();
+ return res;
+ })
+ );
+
+ model.sync(changes);
+ await waitUntilObserved(
+ model.loadingState$,
+ state => state === LoadingState.LOADED
+ );
+ await selectChange(change1);
+ await selectChange(c2);
+ await element.updateComplete;
+
+ assert.isTrue(
+ queryAndAssert<GrButton>(query(element, 'gr-dialog'), '#confirm')
+ .disabled
+ );
+ });
+
test('closing dialog does not trigger reload if no request made', async () => {
const changes: ChangeInfo[] = [change1, change2];
getChangesStub.returns(Promise.resolve(changes));
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list-hashtag-flow/gr-change-list-hashtag-flow.ts b/polygerrit-ui/app/elements/change-list/gr-change-list-hashtag-flow/gr-change-list-hashtag-flow.ts
index f8fcad8..ea61bfc 100644
--- a/polygerrit-ui/app/elements/change-list/gr-change-list-hashtag-flow/gr-change-list-hashtag-flow.ts
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list-hashtag-flow/gr-change-list-hashtag-flow.ts
@@ -159,7 +159,7 @@
.horizontalAlign=${'auto'}
.verticalAlign=${'auto'}
.verticalOffset=${24}
- @opened-changed=${(e: CustomEvent) =>
+ @opened-changed=${(e: ValueChangedEvent<boolean>) =>
(this.isDropdownOpen = e.detail.value)}
>
${when(
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list-hashtag-flow/gr-change-list-hashtag-flow_test.ts b/polygerrit-ui/app/elements/change-list/gr-change-list-hashtag-flow/gr-change-list-hashtag-flow_test.ts
index f7a2531..0cc3e18 100644
--- a/polygerrit-ui/app/elements/change-list/gr-change-list-hashtag-flow/gr-change-list-hashtag-flow_test.ts
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list-hashtag-flow/gr-change-list-hashtag-flow_test.ts
@@ -29,7 +29,6 @@
waitUntilObserved,
} from '../../../test/test-utils';
import {ChangeInfo, NumericChangeId, Hashtag} from '../../../types/common';
-import {EventType} from '../../../types/events';
import {GrAutocomplete} from '../../shared/gr-autocomplete/gr-autocomplete';
import {GrButton} from '../../shared/gr-button/gr-button';
import './gr-change-list-hashtag-flow';
@@ -303,7 +302,7 @@
test('add hashtag from selected change', async () => {
const alertStub = sinon.stub();
- element.addEventListener(EventType.SHOW_ALERT, alertStub);
+ element.addEventListener('show-alert', alertStub);
// selects "hashtag1"
queryAll<HTMLButtonElement>(element, 'button.chip')[0].click();
await element.updateComplete;
@@ -377,7 +376,7 @@
test('add multiple hashtag from selected change', async () => {
const alertStub = sinon.stub();
- element.addEventListener(EventType.SHOW_ALERT, alertStub);
+ element.addEventListener('show-alert', alertStub);
// selects "hashtag1"
queryAll<HTMLButtonElement>(element, 'button.chip')[0].click();
await element.updateComplete;
@@ -425,7 +424,7 @@
test('add existing hashtag not on selected changes', async () => {
const alertStub = sinon.stub();
- element.addEventListener(EventType.SHOW_ALERT, alertStub);
+ element.addEventListener('show-alert', alertStub);
const getHashtagsStub = stubRestApi(
'getChangesWithSimilarHashtag'
@@ -481,7 +480,7 @@
test('add new hashtag', async () => {
const alertStub = sinon.stub();
- element.addEventListener(EventType.SHOW_ALERT, alertStub);
+ element.addEventListener('show-alert', alertStub);
const getHashtagsStub = stubRestApi(
'getChangesWithSimilarHashtag'
@@ -586,7 +585,7 @@
test('cannot add existing hashtag already on selected changes', async () => {
const alertStub = sinon.stub();
- element.addEventListener(EventType.SHOW_ALERT, alertStub);
+ element.addEventListener('show-alert', alertStub);
// selects "sharedHashtag"
queryAll<HTMLButtonElement>(element, 'button.chip')[1].click();
await element.updateComplete;
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list-topic-flow/gr-change-list-topic-flow.ts b/polygerrit-ui/app/elements/change-list/gr-change-list-topic-flow/gr-change-list-topic-flow.ts
index 7752476..4a01412 100644
--- a/polygerrit-ui/app/elements/change-list/gr-change-list-topic-flow/gr-change-list-topic-flow.ts
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list-topic-flow/gr-change-list-topic-flow.ts
@@ -159,7 +159,7 @@
.horizontalAlign=${'auto'}
.verticalAlign=${'auto'}
.verticalOffset=${24}
- @opened-changed=${(e: CustomEvent) =>
+ @opened-changed=${(e: ValueChangedEvent<boolean>) =>
(this.isDropdownOpen = e.detail.value)}
>
${when(
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list-topic-flow/gr-change-list-topic-flow_test.ts b/polygerrit-ui/app/elements/change-list/gr-change-list-topic-flow/gr-change-list-topic-flow_test.ts
index 9125cfd..d2dced2 100644
--- a/polygerrit-ui/app/elements/change-list/gr-change-list-topic-flow/gr-change-list-topic-flow_test.ts
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list-topic-flow/gr-change-list-topic-flow_test.ts
@@ -29,7 +29,6 @@
waitUntilObserved,
} from '../../../test/test-utils';
import {ChangeInfo, NumericChangeId, TopicName} from '../../../types/common';
-import {EventType} from '../../../types/events';
import {GrAutocomplete} from '../../shared/gr-autocomplete/gr-autocomplete';
import {GrButton} from '../../shared/gr-button/gr-button';
import './gr-change-list-topic-flow';
@@ -326,7 +325,7 @@
test('remove single topic', async () => {
const alertStub = sinon.stub();
- element.addEventListener(EventType.SHOW_ALERT, alertStub);
+ element.addEventListener('show-alert', alertStub);
queryAll<HTMLButtonElement>(element, 'button.chip')[0].click();
await element.updateComplete;
queryAndAssert<GrButton>(element, '#remove-topics-button').click();
@@ -387,7 +386,7 @@
test('shows error when remove topic fails', async () => {
const alertStub = sinon.stub();
- element.addEventListener(EventType.SHOW_ALERT, alertStub);
+ element.addEventListener('show-alert', alertStub);
queryAll<HTMLButtonElement>(element, 'button.chip')[0].click();
await element.updateComplete;
queryAndAssert<GrButton>(element, '#remove-topics-button').click();
@@ -435,7 +434,7 @@
test('applies topic to all changes', async () => {
const alertStub = sinon.stub();
- element.addEventListener(EventType.SHOW_ALERT, alertStub);
+ element.addEventListener('show-alert', alertStub);
queryAll<HTMLButtonElement>(element, 'button.chip')[0].click();
await element.updateComplete;
@@ -589,7 +588,7 @@
test('create new topic', async () => {
const alertStub = sinon.stub();
- element.addEventListener(EventType.SHOW_ALERT, alertStub);
+ element.addEventListener('show-alert', alertStub);
const getTopicsStub = stubRestApi('getChangesWithSimilarTopic').resolves(
[]
);
@@ -639,7 +638,7 @@
test('shows error when create topic fails', async () => {
const alertStub = sinon.stub();
- element.addEventListener(EventType.SHOW_ALERT, alertStub);
+ element.addEventListener('show-alert', alertStub);
const getTopicsStub = stubRestApi('getChangesWithSimilarTopic').resolves(
[]
);
@@ -682,7 +681,7 @@
{...createChange(), topic: 'foo' as TopicName},
]);
const alertStub = sinon.stub();
- element.addEventListener(EventType.SHOW_ALERT, alertStub);
+ element.addEventListener('show-alert', alertStub);
const autocomplete = queryAndAssert<GrAutocomplete>(
element,
'gr-autocomplete'
@@ -732,7 +731,7 @@
{...createChange(), topic: 'foo' as TopicName},
]);
const alertStub = sinon.stub();
- element.addEventListener(EventType.SHOW_ALERT, alertStub);
+ element.addEventListener('show-alert', alertStub);
const autocomplete = queryAndAssert<GrAutocomplete>(
element,
'gr-autocomplete'
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view.ts b/polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view.ts
index 1c86354..b5593f3 100644
--- a/polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view.ts
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view.ts
@@ -15,7 +15,7 @@
RepoName,
} from '../../../types/common';
import {ChangeStarToggleStarDetail} from '../../shared/gr-change-star/gr-change-star';
-import {fireAlert, fireEvent, fireTitleChange} from '../../../utils/event-util';
+import {fireAlert, fire, fireTitleChange} from '../../../utils/event-util';
import {getAppContext} from '../../../services/app-context';
import {sharedStyles} from '../../../styles/shared-styles';
import {LitElement, PropertyValues, html, css, nothing} from 'lit';
@@ -316,7 +316,7 @@
e.detail.change._number,
e.detail.starred
);
- fireEvent(this, 'hide-alert');
+ fire(this, 'hide-alert', {});
}
}
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 748c2b8..6c7e661 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
@@ -17,7 +17,7 @@
ServerInfo,
PreferencesInput,
} from '../../../types/common';
-import {fire, fireEvent, fireReload} from '../../../utils/event-util';
+import {fire, fireReload} from '../../../utils/event-util';
import {ColumnNames, ScrollMode} from '../../../constants/constants';
import {getRequirements} from '../../../utils/label-util';
import {Key} from '../../../utils/dom-util';
@@ -77,18 +77,6 @@
@customElement('gr-change-list')
export class GrChangeList extends LitElement {
/**
- * Fired when next page key shortcut was pressed.
- *
- * @event next-page
- */
-
- /**
- * Fired when previous page key shortcut was pressed.
- *
- * @event previous-page
- */
-
- /**
* The logged-in user's account, or an empty object if no user is logged
* in.
*/
@@ -415,11 +403,11 @@
}
private nextPage() {
- fireEvent(this, 'next-page');
+ fire(this, 'next-page', {});
}
private prevPage() {
- fireEvent(this, 'previous-page');
+ fire(this, 'previous-page', {});
}
private refreshChangeList() {
@@ -484,5 +472,9 @@
}
interface HTMLElementEventMap {
'selected-index-changed': ValueChangedEvent<number>;
+ /** Fired when next page key shortcut was pressed. */
+ 'next-page': CustomEvent<{}>;
+ /** Fired when previous page key shortcut was pressed. */
+ 'previous-page': CustomEvent<{}>;
}
}
diff --git a/polygerrit-ui/app/elements/change-list/gr-create-change-help/gr-create-change-help.ts b/polygerrit-ui/app/elements/change-list/gr-create-change-help/gr-create-change-help.ts
index 9c53fea..d9be9ca 100644
--- a/polygerrit-ui/app/elements/change-list/gr-create-change-help/gr-create-change-help.ts
+++ b/polygerrit-ui/app/elements/change-list/gr-create-change-help/gr-create-change-help.ts
@@ -4,7 +4,7 @@
* SPDX-License-Identifier: Apache-2.0
*/
import '../../shared/gr-button/gr-button';
-import {fireEvent} from '../../../utils/event-util';
+import {fire} from '../../../utils/event-util';
import {sharedStyles} from '../../../styles/shared-styles';
import {LitElement, css, html} from 'lit';
import {customElement} from 'lit/decorators.js';
@@ -14,6 +14,10 @@
interface HTMLElementTagNameMap {
'gr-create-change-help': GrCreateChangeHelp;
}
+ interface HTMLElementEventMap {
+ /** Fired when the "Create change" button is tapped. */
+ 'create-tap': CustomEvent<{}>;
+ }
}
@customElement('gr-create-change-help')
@@ -87,11 +91,8 @@
`;
}
- /**
- * Fired when the "Create change" button is tapped.
- */
_handleCreateTap(e: Event) {
e.preventDefault();
- fireEvent(this, 'create-tap');
+ fire(this, 'create-tap', {});
}
}
diff --git a/polygerrit-ui/app/elements/change-list/gr-create-destination-dialog/gr-create-destination-dialog.ts b/polygerrit-ui/app/elements/change-list/gr-create-destination-dialog/gr-create-destination-dialog.ts
index 561fffd..16220ba 100644
--- a/polygerrit-ui/app/elements/change-list/gr-create-destination-dialog/gr-create-destination-dialog.ts
+++ b/polygerrit-ui/app/elements/change-list/gr-create-destination-dialog/gr-create-destination-dialog.ts
@@ -12,6 +12,7 @@
import {assertIsDefined} from '../../../utils/common-util';
import {BindValueChangeEvent} from '../../../types/events';
import {modalStyles} from '../../../styles/gr-modal-styles';
+import {fireNoBubbleNoCompose} from '../../../utils/event-util';
export interface CreateDestinationConfirmDetail {
repo?: RepoName;
@@ -88,7 +89,7 @@
// 'confirm' event here, so let's stop propagation of the bare event.
e.preventDefault();
e.stopPropagation();
- this.dispatchEvent(new CustomEvent('confirm', {detail, bubbles: false}));
+ fireNoBubbleNoCompose(this, 'confirm-destination', detail);
};
}
@@ -96,4 +97,7 @@
interface HTMLElementTagNameMap {
'gr-create-destination-dialog': GrCreateDestinationDialog;
}
+ interface HTMLElementEventMap {
+ 'confirm-destination': CustomEvent<CreateDestinationConfirmDetail>;
+ }
}
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 cd30440..8c99aaa 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
@@ -29,7 +29,7 @@
import {ChangeStarToggleStarDetail} from '../../shared/gr-change-star/gr-change-star';
import {
fireAlert,
- fireEvent,
+ fire,
firePageError,
fireTitleChange,
} from '../../../utils/event-util';
@@ -241,7 +241,9 @@
</dialog>
<gr-create-destination-dialog
id="destinationDialog"
- @confirm=${(e: CustomEvent<CreateDestinationConfirmDetail>) => {
+ @confirm-destination=${(
+ e: CustomEvent<CreateDestinationConfirmDetail>
+ ) => {
this.handleDestinationConfirm(e);
}}
></gr-create-destination-dialog>
@@ -537,7 +539,7 @@
e.detail.change._number,
e.detail.starred
);
- fireEvent(this, 'hide-alert');
+ fire(this, 'hide-alert', {});
if (e.detail.starred) {
this.reporting.reportInteraction('change-starred-from-dashboard');
}
diff --git a/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.ts b/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.ts
index e47b450..36ac307 100644
--- a/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.ts
+++ b/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.ts
@@ -76,7 +76,8 @@
import {
fire,
fireAlert,
- fireEvent,
+ fireError,
+ fireNoBubbleNoCompose,
fireReload,
} from '../../../utils/event-util';
import {
@@ -84,7 +85,6 @@
getVotingRange,
StandardLabels,
} from '../../../utils/label-util';
-import {EventType, ShowAlertEventDetail} from '../../../types/events';
import {
ActionPriority,
ActionType,
@@ -334,18 +334,6 @@
* @event custom-tap - naming pattern: <action key>-tap
*/
- /**
- * Fires to show an alert when a send is attempted on the non-latest patch.
- *
- * @event show-alert
- */
-
- /**
- * Fires when a change action fails.
- *
- * @event show-error
- */
-
@query('#mainContent') mainContent?: Element;
@query('#actionsModal') actionsModal?: HTMLDialogElement;
@@ -671,7 +659,7 @@
id="confirmRebase"
class="confirmDialog"
.changeNumber=${this.change?._number}
- @confirm=${this.handleRebaseConfirm}
+ @confirm-rebase=${this.handleRebaseConfirm}
@cancel=${this.handleConfirmDialogCancel}
.disableActions=${this.inProgressActionKeys.has(
RevisionActions.REBASE
@@ -708,7 +696,7 @@
<gr-confirm-revert-dialog
id="confirmRevertDialog"
class="confirmDialog"
- @confirm=${this.handleRevertDialogConfirm}
+ @confirm-revert=${this.handleRevertDialogConfirm}
@cancel=${this.handleConfirmDialogCancel}
></gr-confirm-revert-dialog>
<gr-confirm-abandon-dialog
@@ -1912,13 +1900,7 @@
) {
if (!response) {
return Promise.resolve(() => {
- this.dispatchEvent(
- new CustomEvent('show-error', {
- detail: {message: `Could not perform action '${action.__key}'`},
- composed: true,
- bubbles: true,
- })
- );
+ fireError(this, `Could not perform action '${action.__key}'`);
});
}
if (action && action.__key === RevisionActions.CHERRYPICK) {
@@ -1936,13 +1918,7 @@
}
}
return response.text().then(errText => {
- this.dispatchEvent(
- new CustomEvent('show-error', {
- detail: {message: `Could not perform action: ${errText}`},
- composed: true,
- bubbles: true,
- })
- );
+ fireError(this, `Could not perform action: ${errText}`);
if (!errText.startsWith('Change is already up to date')) {
throw Error(errText);
}
@@ -1973,19 +1949,13 @@
.fetchChangeUpdates(change)
.then(result => {
if (!result.isLatest) {
- this.dispatchEvent(
- new CustomEvent<ShowAlertEventDetail>(EventType.SHOW_ALERT, {
- detail: {
- message:
- 'Cannot set label: a newer patch has been ' +
- 'uploaded to this change.',
- action: 'Reload',
- callback: () => fireReload(this, true),
- },
- composed: true,
- bubbles: true,
- })
- );
+ fire(this, 'show-alert', {
+ message:
+ 'Cannot set label: a newer patch has been ' +
+ 'uploaded to this change.',
+ action: 'Reload',
+ callback: () => fireReload(this, true),
+ });
// Because this is not a network error, call the cleanup function
// but not the error handler.
@@ -2047,12 +2017,12 @@
// private but used in test
handleDownloadTap() {
- fireEvent(this, 'download-tap');
+ fire(this, 'download-tap', {});
}
// private but used in test
handleIncludedInTap() {
- fireEvent(this, 'included-tap');
+ fire(this, 'included-tap', {});
}
// private but used in test
@@ -2241,17 +2211,21 @@
}
private handleEditTap() {
- this.dispatchEvent(new CustomEvent('edit-tap', {bubbles: false}));
+ fireNoBubbleNoCompose(this, 'edit-tap', {});
}
private handleStopEditTap() {
- this.dispatchEvent(new CustomEvent('stop-edit-tap', {bubbles: false}));
+ fireNoBubbleNoCompose(this, 'stop-edit-tap', {});
}
}
declare global {
interface HTMLElementEventMap {
+ 'download-tap': CustomEvent<{}>;
+ 'edit-tap': CustomEvent<{}>;
+ 'included-tap': CustomEvent<{}>;
'revision-actions-changed': CustomEvent<{value: ActionNameToActionInfoMap}>;
+ 'stop-edit-tap': CustomEvent<{}>;
}
interface HTMLElementTagNameMap {
'gr-change-actions': GrChangeActions;
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 4602eac..82900a3 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
@@ -54,7 +54,6 @@
import {GrConfirmMoveDialog} from '../gr-confirm-move-dialog/gr-confirm-move-dialog';
import {GrConfirmAbandonDialog} from '../gr-confirm-abandon-dialog/gr-confirm-abandon-dialog';
import {GrConfirmRevertDialog} from '../gr-confirm-revert-dialog/gr-confirm-revert-dialog';
-import {EventType} from '../../../types/events';
import {testResolver} from '../../../test/common-test-setup';
import {storageServiceToken} from '../../../services/storage/gr-storage_impl';
import {pluginLoaderToken} from '../../shared/gr-js-api-interface/gr-plugin-loader';
@@ -1456,7 +1455,7 @@
enabled: true,
};
queryAndAssert(element, 'gr-confirm-revert-dialog').dispatchEvent(
- new CustomEvent('confirm', {
+ new CustomEvent('confirm-revert', {
detail: {
message: 'foo message',
revertType: 1,
@@ -2440,7 +2439,7 @@
onShowError = sinon.stub();
element.addEventListener('show-error', onShowError);
onShowAlert = sinon.stub();
- element.addEventListener(EventType.SHOW_ALERT, onShowAlert);
+ element.addEventListener('show-alert', onShowAlert);
});
suite('happy path', () => {
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 b9a04bd..0e71c1f 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
@@ -57,7 +57,7 @@
isSectionSet,
DisplayRules,
} from '../../../utils/change-metadata-util';
-import {fireAlert, fireEvent, fireReload} from '../../../utils/event-util';
+import {fireAlert, fire, fireReload} from '../../../utils/event-util';
import {
EditRevisionInfo,
isDefined,
@@ -769,7 +769,7 @@
} finally {
this.settingTopic = false;
}
- fireEvent(this, 'hide-alert');
+ fire(this, 'hide-alert', {});
fireReload(this);
}
@@ -802,7 +802,7 @@
await this.restApiService.setChangeHashtag(this.change._number, {
add: [newHashtag as Hashtag],
});
- fireEvent(this, 'hide-alert');
+ fire(this, 'hide-alert', {});
fireReload(this);
}
@@ -947,7 +947,7 @@
return createSearchUrl({hashtag, statuses: ['open', 'merged']});
}
- private async handleTopicRemoved(e: CustomEvent) {
+ private async handleTopicRemoved(e: Event) {
assertIsDefined(this.change, 'change');
const target = e.composedPath()[0] as GrLinkedChip;
target.disabled = true;
@@ -957,12 +957,12 @@
} finally {
target.disabled = false;
}
- fireEvent(this, 'hide-alert');
+ fire(this, 'hide-alert', {});
fireReload(this);
}
// private but used in test
- async handleHashtagRemoved(e: CustomEvent) {
+ async handleHashtagRemoved(e: Event) {
e.preventDefault();
assertIsDefined(this.change, 'change');
const target = e.target as GrLinkedChip;
@@ -975,7 +975,7 @@
} finally {
target.disabled = false;
}
- fireEvent(this, 'hide-alert');
+ fire(this, 'hide-alert', {});
fireReload(this);
}
diff --git a/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata_test.ts b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata_test.ts
index e45de51..c46fe24 100644
--- a/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata_test.ts
+++ b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata_test.ts
@@ -54,7 +54,6 @@
import {GrButton} from '../../shared/gr-button/gr-button';
import {nothing} from 'lit';
import {fixture, html, assert} from '@open-wc/testing';
-import {EventType} from '../../../types/events';
import {testResolver} from '../../../test/common-test-setup';
import {pluginLoaderToken} from '../../shared/gr-js-api-interface/gr-plugin-loader';
@@ -871,7 +870,7 @@
Promise.resolve(newTopic)
);
const alertStub = sinon.stub();
- element.addEventListener(EventType.SHOW_ALERT, alertStub);
+ element.addEventListener('show-alert', alertStub);
element.handleTopicChanged(new CustomEvent('test', {detail: newTopic}));
@@ -892,7 +891,7 @@
Promise.resolve(newTopic)
);
const alertStub = sinon.stub();
- element.addEventListener(EventType.SHOW_ALERT, alertStub);
+ element.addEventListener('show-alert', alertStub);
await element.updateComplete;
const chip = queryAndAssert<GrLinkedChip>(element, 'gr-linked-chip');
const remove = queryAndAssert<GrButton>(chip, '#remove');
@@ -916,7 +915,7 @@
Promise.resolve(newHashtag)
);
const alertStub = sinon.stub();
- element.addEventListener(EventType.SHOW_ALERT, alertStub);
+ element.addEventListener('show-alert', alertStub);
element.handleHashtagChanged(
new CustomEvent('test', {detail: 'new hashtag'})
);
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 bb27237..2246671 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
@@ -116,9 +116,9 @@
import {EditRevisionInfo, ParsedChangeInfo} from '../../../types/types';
import {
EditableContentSaveEvent,
- EventType,
+ FileActionTapEvent,
OpenFixPreviewEvent,
- ShowAlertEventDetail,
+ ShowReplyDialogEvent,
SwitchTabEvent,
TabState,
ValueChangedEvent,
@@ -129,7 +129,7 @@
import {
fireAlert,
fireDialogChange,
- fireEvent,
+ fire,
fireReload,
fireTitleChange,
} from '../../../utils/event-util';
@@ -589,11 +589,9 @@
this.addEventListener('editable-content-cancel', () =>
this.handleCommitMessageCancel()
);
- this.addEventListener(EventType.OPEN_FIX_PREVIEW, e =>
- this.onOpenFixPreview(e)
- );
+ this.addEventListener('open-fix-preview', e => this.onOpenFixPreview(e));
- this.addEventListener(EventType.SHOW_TAB, e => this.setActiveTab(e));
+ this.addEventListener('show-tab', e => this.setActiveTab(e));
this.addEventListener('reload', e => {
this.loadData(
/* isLocationChange= */ false,
@@ -1193,7 +1191,6 @@
<gr-download-dialog
id="downloadDialog"
.change=${this.change}
- .patchNum=${this.patchRange?.patchNum}
.config=${this.serverConfig?.download}
@close=${this.handleDownloadDialogClose}
></gr-download-dialog>
@@ -1534,8 +1531,6 @@
.editMode=${this.getEditMode()}
.loggedIn=${this.loggedIn}
.shownFileCount=${this.shownFileCount}
- .patchNum=${this.patchRange?.patchNum}
- .basePatchNum=${this.patchRange?.basePatchNum}
.filesExpanded=${this.fileList?.filesExpanded}
@open-diff-prefs=${this.handleOpenDiffPrefs}
@open-download-dialog=${this.handleOpenDownloadDialog}
@@ -1547,7 +1542,6 @@
id="fileList"
.change=${this.change}
.changeNum=${this.changeNum}
- .patchRange=${this.patchRange}
.editMode=${this.getEditMode()}
@files-shown-changed=${(e: CustomEvent<{length: number}>) => {
this.shownFileCount = e.detail.length;
@@ -2012,7 +2006,7 @@
}
// Private but used in tests.
- handleShowReplyDialog(e: CustomEvent<{value: {ccsOnly: boolean}}>) {
+ handleShowReplyDialog(e: ShowReplyDialogEvent) {
let target = FocusTarget.REVIEWERS;
if (e.detail.value && e.detail.value.ccsOnly) {
target = FocusTarget.CCS;
@@ -2167,7 +2161,7 @@
} else if (this.viewState?.commentId) {
tab = Tab.COMMENT_THREADS;
}
- this.setActiveTab(new CustomEvent(EventType.SHOW_TAB, {detail: {tab}}));
+ this.setActiveTab(new CustomEvent('show-tab', {detail: {tab}}));
}
// Private but used in tests.
@@ -2358,7 +2352,7 @@
private handleOpenReplyDialog() {
if (!this.loggedIn) {
- fireEvent(this, 'show-auth-required');
+ fire(this, 'show-auth-required', {});
return;
}
this.openReplyDialog(FocusTarget.ANY);
@@ -2388,7 +2382,7 @@
reason
)
.then(() => {
- fireEvent(this, 'hide-alert');
+ fire(this, 'hide-alert', {});
});
} else {
const reason = getAddedByReason(this.account, this.serverConfig);
@@ -2405,7 +2399,7 @@
reason
)
.then(() => {
- fireEvent(this, 'hide-alert');
+ fire(this, 'hide-alert', {});
});
}
this.change = newChange;
@@ -2873,7 +2867,7 @@
allDataPromises.push(mergeabilityLoaded);
coreDataPromise.then(() => {
- fireEvent(this, 'change-details-loaded');
+ fire(this, 'change-details-loaded', {});
this.reporting.timeEnd(Timing.CHANGE_RELOAD);
if (isLocationChange) {
this.reporting.changeDisplayed(roleDetails(this.change, this.account));
@@ -3058,20 +3052,14 @@
}
this.cancelUpdateCheckTimer();
- this.dispatchEvent(
- new CustomEvent<ShowAlertEventDetail>(EventType.SHOW_ALERT, {
- detail: {
- message: toastMessage,
- // Persist this alert.
- dismissOnNavigation: true,
- showDismiss: true,
- action: 'Reload',
- callback: () => fireReload(this, true),
- },
- composed: true,
- bubbles: true,
- })
- );
+ fire(this, 'show-alert', {
+ message: toastMessage,
+ // Persist this alert.
+ dismissOnNavigation: true,
+ showDismiss: true,
+ action: 'Reload',
+ callback: () => fireReload(this, true),
+ });
});
}, this.serverConfig.change.update_delay * 1000);
}
@@ -3100,7 +3088,7 @@
return classes.join(' ');
}
- private handleFileActionTap(e: CustomEvent<{path: string; action: string}>) {
+ private handleFileActionTap(e: FileActionTapEvent) {
e.preventDefault();
assertIsDefined(this.fileListHeader);
const controls =
@@ -3220,7 +3208,7 @@
e.detail.change._number,
e.detail.starred
);
- fireEvent(this, 'hide-alert');
+ fire(this, 'hide-alert', {});
}
private getRevisionInfo(): RevisionInfoClass | undefined {
@@ -3248,6 +3236,7 @@
declare global {
interface HTMLElementEventMap {
'toggle-star': CustomEvent<ChangeStarToggleStarDetail>;
+ 'change-details-loaded': CustomEvent<{}>;
}
interface HTMLElementTagNameMap {
'gr-change-view': GrChangeView;
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 ac4c1f9..34d11a3 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
@@ -1772,7 +1772,7 @@
const openStub = sinon.stub(element, 'openReplyDialog');
const e = new CustomEvent('show-reply-dialog', {
- detail: {value: {ccsOnly: false}},
+ detail: {value: {reviewersOnly: true, ccsOnly: false}},
});
element.handleShowReplyDialog(e);
assert(
@@ -1781,7 +1781,7 @@
);
assert.equal(openStub.callCount, 1);
- e.detail.value = {ccsOnly: true};
+ e.detail.value = {reviewersOnly: false, ccsOnly: true};
element.handleShowReplyDialog(e);
assert(
openStub.lastCall.calledWithExactly(FocusTarget.CCS),
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 85746df..2129918 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
@@ -14,6 +14,7 @@
import {BindValueChangeEvent} from '../../../types/events';
import {ShortcutController} from '../../lit/shortcut-controller';
import {ChangeActionDialog} from '../../../types/common';
+import {fireNoBubble} from '../../../utils/event-util';
declare global {
interface HTMLElementTagNameMap {
@@ -132,25 +133,14 @@
// private but used in test
confirm() {
- this.dispatchEvent(
- new CustomEvent('confirm', {
- detail: {reason: this.message},
- composed: true,
- bubbles: false,
- })
- );
+ fireNoBubble(this, 'confirm', {});
}
// private but used in test
handleCancelTap(e: Event) {
e.preventDefault();
e.stopPropagation();
- this.dispatchEvent(
- new CustomEvent('cancel', {
- composed: true,
- bubbles: false,
- })
- );
+ fireNoBubble(this, 'cancel', {});
}
private handleBindValueChanged(e: BindValueChangeEvent) {
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-conflict-dialog/gr-confirm-cherrypick-conflict-dialog.ts b/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-conflict-dialog/gr-confirm-cherrypick-conflict-dialog.ts
index 02156df..7723327 100644
--- a/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-conflict-dialog/gr-confirm-cherrypick-conflict-dialog.ts
+++ b/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-conflict-dialog/gr-confirm-cherrypick-conflict-dialog.ts
@@ -7,6 +7,7 @@
import {customElement} from 'lit/decorators.js';
import {sharedStyles} from '../../../styles/shared-styles';
import {ChangeActionDialog} from '../../../types/common';
+import {fireNoBubble} from '../../../utils/event-util';
import '../../shared/gr-dialog/gr-dialog';
@customElement('gr-confirm-cherrypick-conflict-dialog')
@@ -66,23 +67,13 @@
handleConfirmTap(e: Event) {
e.preventDefault();
e.stopPropagation();
- this.dispatchEvent(
- new CustomEvent('confirm', {
- composed: true,
- bubbles: false,
- })
- );
+ fireNoBubble(this, 'confirm', {});
}
handleCancelTap(e: Event) {
e.preventDefault();
e.stopPropagation();
- this.dispatchEvent(
- new CustomEvent('cancel', {
- composed: true,
- bubbles: false,
- })
- );
+ fireNoBubble(this, 'cancel', {});
}
}
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 5f3b824..4d2602e 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
@@ -30,7 +30,7 @@
ChangeStatus,
ProgressStatus,
} from '../../../constants/constants';
-import {fireEvent} from '../../../utils/event-util';
+import {fire, fireNoBubble} from '../../../utils/event-util';
import {css, html, LitElement, PropertyValues} from 'lit';
import {sharedStyles} from '../../../styles/shared-styles';
import {choose} from 'lit/directives/choose.js';
@@ -503,12 +503,12 @@
private handlecherryPickSingleChangeClicked() {
this.cherryPickType = CherryPickType.SINGLE_CHANGE;
- fireEvent(this, 'iron-resize');
+ fire(this, 'iron-resize', {});
}
private handlecherryPickTopicClicked() {
this.cherryPickType = CherryPickType.TOPIC;
- fireEvent(this, 'iron-resize');
+ fire(this, 'iron-resize', {});
}
private computeMessage() {
@@ -605,23 +605,13 @@
return;
}
// Cherry pick single change
- this.dispatchEvent(
- new CustomEvent('confirm', {
- composed: true,
- bubbles: false,
- })
- );
+ fireNoBubble(this, 'confirm', {});
}
private handleCancelTap(e: Event) {
e.preventDefault();
e.stopPropagation();
- this.dispatchEvent(
- new CustomEvent('cancel', {
- composed: true,
- bubbles: false,
- })
- );
+ fireNoBubble(this, 'cancel', {});
}
resetFocus() {
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 3f84189..6f82e8c 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
@@ -15,6 +15,7 @@
import {ValueChangedEvent} from '../../../types/events';
import {ShortcutController} from '../../lit/shortcut-controller';
import {throwingErrorCallback} from '../../shared/gr-rest-api-interface/gr-rest-apis/gr-rest-api-helper';
+import {fireNoBubble} from '../../../utils/event-util';
const SUGGESTIONS_LIMIT = 15;
@@ -142,23 +143,13 @@
private handleConfirmTap(e: Event) {
e.preventDefault();
e.stopPropagation();
- this.dispatchEvent(
- new CustomEvent('confirm', {
- composed: true,
- bubbles: false,
- })
- );
+ fireNoBubble(this, 'confirm', {});
}
private handleCancelTap(e: Event) {
e.preventDefault();
e.stopPropagation();
- this.dispatchEvent(
- new CustomEvent('cancel', {
- composed: true,
- bubbles: false,
- })
- );
+ fireNoBubble(this, 'cancel', {});
}
private getProjectBranchesSuggestions(input: string) {
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-rebase-dialog/gr-confirm-rebase-dialog.ts b/polygerrit-ui/app/elements/change/gr-confirm-rebase-dialog/gr-confirm-rebase-dialog.ts
index c2739f3..34869f2 100644
--- a/polygerrit-ui/app/elements/change/gr-confirm-rebase-dialog/gr-confirm-rebase-dialog.ts
+++ b/polygerrit-ui/app/elements/change/gr-confirm-rebase-dialog/gr-confirm-rebase-dialog.ts
@@ -22,7 +22,7 @@
import {sharedStyles} from '../../../styles/shared-styles';
import {ValueChangedEvent} from '../../../types/events';
import {throwingErrorCallback} from '../../shared/gr-rest-api-interface/gr-rest-apis/gr-rest-api-helper';
-import {KnownExperimentId} from '../../../services/flags/flags';
+import {fireNoBubbleNoCompose} from '../../../utils/event-util';
export interface RebaseChange {
name: string;
@@ -99,8 +99,6 @@
private readonly restApiService = getAppContext().restApiService;
- private readonly flagsService = getAppContext().flagsService;
-
constructor() {
super();
this.query = input => this.getChangeSuggestions(input);
@@ -234,8 +232,7 @@
>
</div>
${when(
- this.flagsService.isEnabled(KnownExperimentId.REBASE_CHAIN) &&
- this.hasParent,
+ this.hasParent,
() =>
html`<div>
<input
@@ -355,14 +352,14 @@
allowConflicts: this.rebaseAllowConflicts.checked,
rebaseChain: !!this.rebaseChain?.checked,
};
- this.dispatchEvent(new CustomEvent('confirm', {detail}));
+ fireNoBubbleNoCompose(this, 'confirm-rebase', detail);
this.text = '';
}
private handleCancelTap(e: Event) {
e.preventDefault();
e.stopPropagation();
- this.dispatchEvent(new CustomEvent('cancel'));
+ fireNoBubbleNoCompose(this, 'cancel', {});
this.text = '';
}
@@ -398,4 +395,7 @@
interface HTMLElementTagNameMap {
'gr-confirm-rebase-dialog': GrConfirmRebaseDialog;
}
+ interface HTMLElementEventMap {
+ 'confirm-rebase': CustomEvent<ConfirmRebaseEventDetail>;
+ }
}
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-revert-dialog/gr-confirm-revert-dialog.ts b/polygerrit-ui/app/elements/change/gr-confirm-revert-dialog/gr-confirm-revert-dialog.ts
index dd9a5ee..fedc377 100644
--- a/polygerrit-ui/app/elements/change/gr-confirm-revert-dialog/gr-confirm-revert-dialog.ts
+++ b/polygerrit-ui/app/elements/change/gr-confirm-revert-dialog/gr-confirm-revert-dialog.ts
@@ -30,21 +30,6 @@
message?: string;
}
-export interface CancelRevertEventDetail {
- revertType: RevertType;
-}
-
-declare global {
- interface HTMLElementEventMap {
- /** Fired when the confirm button is pressed. */
- // prettier-ignore
- 'confirm': CustomEvent<ConfirmRevertEventDetail>;
- /** Fired when the cancel button is pressed. */
- // prettier-ignore
- 'cancel': CustomEvent<CancelRevertEventDetail>;
- }
-}
-
@customElement('gr-confirm-revert-dialog')
export class GrConfirmRevertDialog
extends LitElement
@@ -302,16 +287,13 @@
revertType: this.revertType,
message: this.message,
};
- fire(this, 'confirm', detail);
+ fire(this, 'confirm-revert', detail);
}
private handleCancelTap(e: Event) {
e.preventDefault();
e.stopPropagation();
- const detail: ConfirmRevertEventDetail = {
- revertType: this.revertType,
- };
- fire(this, 'cancel', detail);
+ fire(this, 'cancel', {});
}
}
@@ -319,4 +301,7 @@
interface HTMLElementTagNameMap {
'gr-confirm-revert-dialog': GrConfirmRevertDialog;
}
+ interface HTMLElementEventMap {
+ 'confirm-revert': CustomEvent<ConfirmRevertEventDetail>;
+ }
}
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-revert-dialog/gr-confirm-revert-dialog_test.ts b/polygerrit-ui/app/elements/change/gr-confirm-revert-dialog/gr-confirm-revert-dialog_test.ts
index 38309f2..904285f 100644
--- a/polygerrit-ui/app/elements/change/gr-confirm-revert-dialog/gr-confirm-revert-dialog_test.ts
+++ b/polygerrit-ui/app/elements/change/gr-confirm-revert-dialog/gr-confirm-revert-dialog_test.ts
@@ -7,7 +7,6 @@
import '../../../test/common-test-setup';
import {createChange} from '../../../test/test-data-generators';
import {ChangeSubmissionId, CommitId} from '../../../types/common';
-import {EventType} from '../../../types/events';
import './gr-confirm-revert-dialog';
import {GrConfirmRevertDialog} from './gr-confirm-revert-dialog';
@@ -47,7 +46,7 @@
test('no match', () => {
assert.isNotOk(element.message);
const alertStub = sinon.stub();
- element.addEventListener(EventType.SHOW_ALERT, alertStub);
+ element.addEventListener('show-alert', alertStub);
element.populateRevertSingleChangeMessage(
createChange(),
'not a commitHash in sight',
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 1b5f171..b5297fd 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
@@ -21,6 +21,7 @@
import {commentsModelToken} from '../../../models/comments/comments-model';
import {changeModelToken} from '../../../models/change/change-model';
import {resolve} from '../../../models/dependency';
+import {fireNoBubbleNoCompose} from '../../../utils/event-util';
@customElement('gr-confirm-submit-dialog')
export class GrConfirmSubmitDialog
@@ -193,13 +194,13 @@
private handleConfirmTap(e: Event) {
e.preventDefault();
e.stopPropagation();
- this.dispatchEvent(new CustomEvent('confirm', {bubbles: false}));
+ fireNoBubbleNoCompose(this, 'confirm', {});
}
private handleCancelTap(e: Event) {
e.preventDefault();
e.stopPropagation();
- this.dispatchEvent(new CustomEvent('cancel', {bubbles: false}));
+ fireNoBubbleNoCompose(this, 'cancel', {});
}
}
diff --git a/polygerrit-ui/app/elements/change/gr-download-dialog/gr-download-dialog.ts b/polygerrit-ui/app/elements/change/gr-download-dialog/gr-download-dialog.ts
index d291ebb..11dc890 100644
--- a/polygerrit-ui/app/elements/change/gr-download-dialog/gr-download-dialog.ts
+++ b/polygerrit-ui/app/elements/change/gr-download-dialog/gr-download-dialog.ts
@@ -9,7 +9,7 @@
import {GrDownloadCommands} from '../../shared/gr-download-commands/gr-download-commands';
import {GrButton} from '../../shared/gr-button/gr-button';
import {copyToClipbard, hasOwnProperty} from '../../../utils/common-util';
-import {fireEvent} from '../../../utils/event-util';
+import {fire} from '../../../utils/event-util';
import {fontStyles} from '../../../styles/gr-font-styles';
import {sharedStyles} from '../../../styles/shared-styles';
import {LitElement, PropertyValues, html, css} from 'lit';
@@ -17,6 +17,9 @@
import {assertIsDefined} from '../../../utils/common-util';
import {BindValueChangeEvent} from '../../../types/events';
import {ShortcutController} from '../../lit/shortcut-controller';
+import {subscribe} from '../../lit/subscription-controller';
+import {resolve} from '../../../models/dependency';
+import {changeModelToken} from '../../../models/change/change-model';
@customElement('gr-download-dialog')
export class GrDownloadDialog extends LitElement {
@@ -38,15 +41,21 @@
@property({type: Object})
config?: DownloadInfo;
- @property({type: String})
- patchNum: PatchSetNum | undefined;
+ @state() patchNum?: PatchSetNum;
@state() private selectedScheme?: string;
private readonly shortcuts = new ShortcutController(this);
+ private readonly getChangeModel = resolve(this, changeModelToken);
+
constructor() {
super();
+ subscribe(
+ this,
+ () => this.getChangeModel().patchNum$,
+ x => (this.patchNum = x)
+ );
for (const key of ['1', '2', '3', '4', '5']) {
this.shortcuts.addLocal({key}, e => this.handleNumberKey(e));
}
@@ -224,7 +233,7 @@
commands[index].command,
`${commands[index].title} command`
);
- fireEvent(this, 'close');
+ fire(this, 'close', {});
}
override focus() {
@@ -314,7 +323,7 @@
private handleCloseTap(e: Event) {
e.preventDefault();
e.stopPropagation();
- fireEvent(this, 'close');
+ fire(this, 'close', {});
}
private schemesChanged() {
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 c1e866c..69297f6 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
@@ -25,7 +25,7 @@
import {DiffPreferencesInfo} from '../../../types/diff';
import {GrDiffModeSelector} from '../../../embed/diff/gr-diff-mode-selector/gr-diff-mode-selector';
import {GrButton} from '../../shared/gr-button/gr-button';
-import {fireEvent} from '../../../utils/event-util';
+import {fire, fireNoBubbleNoCompose} from '../../../utils/event-util';
import {css, html, LitElement} from 'lit';
import {sharedStyles} from '../../../styles/shared-styles';
import {when} from 'lit/directives/when.js';
@@ -40,25 +40,11 @@
import {configModelToken} from '../../../models/config/config-model';
import {createChangeUrl} from '../../../models/views/change';
import {userModelToken} from '../../../models/user/user-model';
+import {changeModelToken} from '../../../models/change/change-model';
+import {PatchRangeChangeEvent} from '../../diff/gr-patch-range-select/gr-patch-range-select';
@customElement('gr-file-list-header')
export class GrFileListHeader extends LitElement {
- /**
- * @event expand-diffs
- */
-
- /**
- * @event collapse-diffs
- */
-
- /**
- * @event open-diff-prefs
- */
-
- /**
- * @event open-download-dialog
- */
-
@property({type: Object})
account: AccountInfo | undefined;
@@ -84,14 +70,12 @@
shownFileCount = 0;
@property({type: String})
- patchNum?: PatchSetNum;
-
- @property({type: String})
- basePatchNum?: BasePatchSetNum;
-
- @property({type: String})
filesExpanded?: FilesExpandedState;
+ @state() patchNum?: PatchSetNum;
+
+ @state() basePatchNum?: BasePatchSetNum;
+
@state()
diffPrefs?: DiffPreferencesInfo;
@@ -119,6 +103,8 @@
private readonly getNavigation = resolve(this, navigationToken);
+ private readonly getChangeModel = resolve(this, changeModelToken);
+
constructor() {
super();
subscribe(
@@ -136,6 +122,16 @@
this.serverConfig = config;
}
);
+ subscribe(
+ this,
+ () => this.getChangeModel().patchNum$,
+ x => (this.patchNum = x)
+ );
+ subscribe(
+ this,
+ () => this.getChangeModel().basePatchNum$,
+ x => (this.basePatchNum = x)
+ );
}
static override styles = [
@@ -366,11 +362,11 @@
}
private expandAllDiffs() {
- fireEvent(this, 'expand-diffs');
+ fire(this, 'expand-diffs', {});
}
private collapseAllDiffs() {
- fireEvent(this, 'collapse-diffs');
+ fire(this, 'collapse-diffs', {});
}
private computeExpandedClass(filesExpanded?: FilesExpandedState) {
@@ -392,7 +388,7 @@
return shownFileCount <= maxFilesForBulkActions;
}
- handlePatchChange(e: CustomEvent) {
+ handlePatchChange(e: PatchRangeChangeEvent) {
const {basePatchNum, patchNum} = e.detail;
if (
(basePatchNum === this.basePatchNum && patchNum === this.patchNum) ||
@@ -407,15 +403,13 @@
private handlePrefsTap(e: Event) {
e.preventDefault();
- fireEvent(this, 'open-diff-prefs');
+ fire(this, 'open-diff-prefs', {});
}
private handleDownloadTap(e: Event) {
e.preventDefault();
e.stopPropagation();
- this.dispatchEvent(
- new CustomEvent('open-download-dialog', {bubbles: false})
- );
+ fireNoBubbleNoCompose(this, 'open-download-dialog', {});
}
private computeEditModeClass(editMode?: boolean) {
@@ -439,4 +433,10 @@
interface HTMLElementTagNameMap {
'gr-file-list-header': GrFileListHeader;
}
+ interface HTMLElementEventMap {
+ 'collapse-diffs': CustomEvent<{}>;
+ 'expand-diffs': CustomEvent<{}>;
+ 'open-diff-prefs': CustomEvent<{}>;
+ 'open-download-dialog': CustomEvent<{}>;
+ }
}
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 6eaf7ae..ad75253 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
@@ -42,6 +42,7 @@
NumericChangeId,
PARENT,
PatchRange,
+ RevisionPatchSetNum,
} from '../../../types/common';
import {DiffPreferencesInfo} from '../../../types/diff';
import {GrDiffHost} from '../../diff/gr-diff-host/gr-diff-host';
@@ -173,19 +174,24 @@
}
@customElement('gr-file-list')
export class GrFileList extends LitElement {
- /**
- * @event files-expanded-changed
- * @event files-shown-changed
- * @event diff-prefs-changed
- */
@query('#diffPreferencesDialog')
diffPreferencesDialog?: GrDiffPreferencesDialog;
- @property({type: Object})
- patchRange?: PatchRange;
+ get patchRange(): PatchRange | undefined {
+ if (!this.patchNum) return undefined;
+ return {
+ patchNum: this.patchNum,
+ basePatchNum: this.basePatchNum,
+ };
+ }
- @property({type: String})
- patchNum?: string;
+ // Private but used in tests.
+ @state()
+ patchNum?: RevisionPatchSetNum;
+
+ // Private but used in tests.
+ @state()
+ basePatchNum: BasePatchSetNum = PARENT;
@property({type: Number})
changeNum?: NumericChangeId;
@@ -811,6 +817,16 @@
this.reviewed = reviewedFiles ?? [];
}
);
+ subscribe(
+ this,
+ () => this.getChangeModel().patchNum$,
+ x => (this.patchNum = x)
+ );
+ subscribe(
+ this,
+ () => this.getChangeModel().basePatchNum$,
+ x => (this.basePatchNum = x)
+ );
}
override willUpdate(changedProperties: PropertyValues): void {
@@ -1136,7 +1152,7 @@
const hasExtendedStatus = this.filesLeftBase.length > 0;
// no file means "header row"
if (!file) {
- const psNum = this.patchRange?.patchNum;
+ const psNum = this.patchNum;
return hasExtendedStatus
? this.renderDivWithTooltip(`${psNum}`, `Patchset ${psNum}`)
: nothing;
@@ -1154,8 +1170,8 @@
const status = fileIsReverted
? FileInfoStatus.REVERTED
: file?.status ?? FileInfoStatus.MODIFIED;
- const left = `patchset ${this.patchRange?.basePatchNum}`;
- const right = `patchset ${this.patchRange?.patchNum}`;
+ const left = `patchset ${this.basePatchNum}`;
+ const right = `patchset ${this.patchNum}`;
const postfix = ` between ${left} and ${right}`;
return html`<gr-file-status
@@ -1175,7 +1191,7 @@
></gr-icon>
`;
// no path means "header row"
- const psNum = this.patchRange?.basePatchNum;
+ const psNum = this.basePatchNum;
if (!path) {
return html`
${this.renderDivWithTooltip(`${psNum}`, `Patchset ${psNum}`)} ${arrow}
@@ -1187,7 +1203,7 @@
const status = file.status ?? FileInfoStatus.MODIFIED;
const left = 'base';
- const right = `patchset ${this.patchRange?.basePatchNum}`;
+ const right = `patchset ${this.basePatchNum}`;
const postfix = ` between ${left} and ${right}`;
return html`
@@ -1664,16 +1680,16 @@
if (
this.change &&
this.changeNum &&
- this.patchRange?.patchNum &&
- new RevisionInfo(this.change).isMergeCommit(this.patchRange.patchNum) &&
- this.patchRange.basePatchNum === PARENT &&
- this.patchRange.patchNum !== EDIT
+ this.patchNum &&
+ new RevisionInfo(this.change).isMergeCommit(this.patchNum) &&
+ this.basePatchNum === PARENT &&
+ this.patchNum !== EDIT
) {
const allFilesByPath = await this.restApiService.getChangeOrEditFiles(
this.changeNum,
{
basePatchNum: -1 as BasePatchSetNum, // -1 is first (target) parent
- patchNum: this.patchRange.patchNum,
+ patchNum: this.patchNum,
}
);
if (!allFilesByPath) return;
@@ -2252,13 +2268,7 @@
const previousNumFilesShown = this.shownFiles ? this.shownFiles.length : 0;
const filesShown = this.files.slice(0, this.numFilesShown);
- this.dispatchEvent(
- new CustomEvent('files-shown-changed', {
- detail: {length: filesShown.length},
- composed: true,
- bubbles: true,
- })
- );
+ fire(this, 'files-shown-changed', {length: filesShown.length});
// Start the timer for the rendering work here because this is where the
// shownFiles property is being set, and shownFiles is used in the
diff --git a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_test.ts b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_test.ts
index c79be96..8fb5622 100644
--- a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_test.ts
+++ b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_test.ts
@@ -102,10 +102,8 @@
ignore_whitespace: 'IGNORE_NONE',
};
element.numFilesShown = 200;
- element.patchRange = {
- basePatchNum: PARENT,
- patchNum: 2 as RevisionPatchSetNum,
- };
+ element.basePatchNum = PARENT;
+ element.patchNum = 2 as RevisionPatchSetNum;
saveStub = sinon
.stub(element, '_saveReviewedState')
.callsFake(() => Promise.resolve());
@@ -365,7 +363,7 @@
test('renders file status column header', async () => {
element.files = createFiles(1, {lines_inserted: 9});
element.filesLeftBase = createFiles(1, {lines_inserted: 9});
- element.patchRange!.basePatchNum = 1 as PatchSetNumber;
+ element.basePatchNum = 1 as PatchSetNumber;
await element.updateComplete;
const fileRows = queryAll<HTMLDivElement>(element, '.header-row');
const statusCol = queryAndAssert(fileRows?.[0], '.status');
@@ -695,22 +693,9 @@
test('comment filtering', () => {
element.changeComments = createChangeComments();
- const parentTo1 = {
- basePatchNum: PARENT,
- patchNum: 1 as RevisionPatchSetNum,
- };
- const parentTo2 = {
- basePatchNum: PARENT,
- patchNum: 2 as RevisionPatchSetNum,
- };
-
- const _1To2 = {
- basePatchNum: 1 as BasePatchSetNum,
- patchNum: 2 as RevisionPatchSetNum,
- };
-
- element.patchRange = parentTo1;
+ element.basePatchNum = PARENT;
+ element.patchNum = 1 as RevisionPatchSetNum;
assert.equal(
element.computeCommentsStringMobile({
__path: '/COMMIT_MSG',
@@ -719,7 +704,9 @@
}),
'2c'
);
- element.patchRange = _1To2;
+
+ element.basePatchNum = 1 as BasePatchSetNum;
+ element.patchNum = 2 as RevisionPatchSetNum;
assert.equal(
element.computeCommentsStringMobile({
__path: '/COMMIT_MSG',
@@ -728,7 +715,9 @@
}),
'3c'
);
- element.patchRange = parentTo1;
+
+ element.basePatchNum = PARENT;
+ element.patchNum = 1 as RevisionPatchSetNum;
assert.equal(
element.computeDraftsString({
__path: 'unresolved.file',
@@ -737,7 +726,9 @@
}),
'1 draft'
);
- element.patchRange = _1To2;
+
+ element.basePatchNum = 1 as BasePatchSetNum;
+ element.patchNum = 2 as RevisionPatchSetNum;
assert.equal(
element.computeDraftsString({
__path: 'unresolved.file',
@@ -746,7 +737,9 @@
}),
'1 draft'
);
- element.patchRange = parentTo1;
+
+ element.basePatchNum = PARENT;
+ element.patchNum = 1 as RevisionPatchSetNum;
assert.equal(
element.computeDraftsStringMobile({
__path: 'unresolved.file',
@@ -755,7 +748,9 @@
}),
'1d'
);
- element.patchRange = _1To2;
+
+ element.basePatchNum = 1 as BasePatchSetNum;
+ element.patchNum = 2 as RevisionPatchSetNum;
assert.equal(
element.computeDraftsStringMobile({
__path: 'unresolved.file',
@@ -764,7 +759,9 @@
}),
'1d'
);
- element.patchRange = parentTo1;
+
+ element.basePatchNum = PARENT;
+ element.patchNum = 1 as RevisionPatchSetNum;
assert.equal(
element.computeCommentsStringMobile({
__path: 'myfile.txt',
@@ -773,7 +770,9 @@
}),
'1c'
);
- element.patchRange = _1To2;
+
+ element.basePatchNum = 1 as BasePatchSetNum;
+ element.patchNum = 2 as RevisionPatchSetNum;
assert.equal(
element.computeCommentsStringMobile({
__path: 'myfile.txt',
@@ -782,16 +781,9 @@
}),
'3c'
);
- element.patchRange = parentTo1;
- assert.equal(
- element.computeDraftsString({
- __path: 'myfile.txt',
- size: 0,
- size_delta: 0,
- }),
- ''
- );
- element.patchRange = _1To2;
+
+ element.basePatchNum = PARENT;
+ element.patchNum = 1 as RevisionPatchSetNum;
assert.equal(
element.computeDraftsString({
__path: 'myfile.txt',
@@ -801,7 +793,19 @@
''
);
- element.patchRange = parentTo1;
+ element.basePatchNum = 1 as BasePatchSetNum;
+ element.patchNum = 2 as RevisionPatchSetNum;
+ assert.equal(
+ element.computeDraftsString({
+ __path: 'myfile.txt',
+ size: 0,
+ size_delta: 0,
+ }),
+ ''
+ );
+
+ element.basePatchNum = PARENT;
+ element.patchNum = 1 as RevisionPatchSetNum;
assert.equal(
element.computeDraftsStringMobile({
__path: 'myfile.txt',
@@ -810,7 +814,9 @@
}),
''
);
- element.patchRange = _1To2;
+
+ element.basePatchNum = 1 as BasePatchSetNum;
+ element.patchNum = 2 as RevisionPatchSetNum;
assert.equal(
element.computeDraftsStringMobile({
__path: 'myfile.txt',
@@ -819,7 +825,9 @@
}),
''
);
- element.patchRange = parentTo1;
+
+ element.basePatchNum = PARENT;
+ element.patchNum = 1 as RevisionPatchSetNum;
assert.equal(
element.computeCommentsStringMobile({
__path: 'file_added_in_rev2.txt',
@@ -828,7 +836,9 @@
}),
''
);
- element.patchRange = _1To2;
+
+ element.basePatchNum = 1 as BasePatchSetNum;
+ element.patchNum = 2 as RevisionPatchSetNum;
assert.equal(
element.computeCommentsStringMobile({
__path: 'file_added_in_rev2.txt',
@@ -837,7 +847,9 @@
}),
''
);
- element.patchRange = parentTo1;
+
+ element.basePatchNum = PARENT;
+ element.patchNum = 1 as RevisionPatchSetNum;
assert.equal(
element.computeDraftsString({
__path: 'file_added_in_rev2.txt',
@@ -846,7 +858,9 @@
}),
''
);
- element.patchRange = _1To2;
+
+ element.basePatchNum = 1 as BasePatchSetNum;
+ element.patchNum = 2 as RevisionPatchSetNum;
assert.equal(
element.computeDraftsString({
__path: 'file_added_in_rev2.txt',
@@ -855,7 +869,9 @@
}),
''
);
- element.patchRange = parentTo1;
+
+ element.basePatchNum = PARENT;
+ element.patchNum = 1 as RevisionPatchSetNum;
assert.equal(
element.computeDraftsStringMobile({
__path: 'file_added_in_rev2.txt',
@@ -864,7 +880,9 @@
}),
''
);
- element.patchRange = _1To2;
+
+ element.basePatchNum = 1 as BasePatchSetNum;
+ element.patchNum = 2 as RevisionPatchSetNum;
assert.equal(
element.computeDraftsStringMobile({
__path: 'file_added_in_rev2.txt',
@@ -873,7 +891,9 @@
}),
''
);
- element.patchRange = parentTo2;
+
+ element.basePatchNum = PARENT;
+ element.patchNum = 2 as RevisionPatchSetNum;
assert.equal(
element.computeCommentsStringMobile({
__path: '/COMMIT_MSG',
@@ -882,7 +902,9 @@
}),
'1c'
);
- element.patchRange = _1To2;
+
+ element.basePatchNum = 1 as BasePatchSetNum;
+ element.patchNum = 2 as RevisionPatchSetNum;
assert.equal(
element.computeCommentsStringMobile({
__path: '/COMMIT_MSG',
@@ -891,7 +913,9 @@
}),
'3c'
);
- element.patchRange = parentTo1;
+
+ element.basePatchNum = PARENT;
+ element.patchNum = 1 as RevisionPatchSetNum;
assert.equal(
element.computeDraftsString({
__path: '/COMMIT_MSG',
@@ -900,7 +924,9 @@
}),
'2 drafts'
);
- element.patchRange = _1To2;
+
+ element.basePatchNum = 1 as BasePatchSetNum;
+ element.patchNum = 2 as RevisionPatchSetNum;
assert.equal(
element.computeDraftsString({
__path: '/COMMIT_MSG',
@@ -909,7 +935,9 @@
}),
'2 drafts'
);
- element.patchRange = parentTo1;
+
+ element.basePatchNum = PARENT;
+ element.patchNum = 1 as RevisionPatchSetNum;
assert.equal(
element.computeDraftsStringMobile({
__path: '/COMMIT_MSG',
@@ -918,7 +946,9 @@
}),
'2d'
);
- element.patchRange = _1To2;
+
+ element.basePatchNum = 1 as BasePatchSetNum;
+ element.patchNum = 2 as RevisionPatchSetNum;
assert.equal(
element.computeDraftsStringMobile({
__path: '/COMMIT_MSG',
@@ -927,7 +957,9 @@
}),
'2d'
);
- element.patchRange = parentTo2;
+
+ element.basePatchNum = PARENT;
+ element.patchNum = 2 as RevisionPatchSetNum;
assert.equal(
element.computeCommentsStringMobile({
__path: 'myfile.txt',
@@ -936,7 +968,9 @@
}),
'2c'
);
- element.patchRange = _1To2;
+
+ element.basePatchNum = 1 as BasePatchSetNum;
+ element.patchNum = 2 as RevisionPatchSetNum;
assert.equal(
element.computeCommentsStringMobile({
__path: 'myfile.txt',
@@ -945,7 +979,9 @@
}),
'3c'
);
- element.patchRange = parentTo2;
+
+ element.basePatchNum = PARENT;
+ element.patchNum = 2 as RevisionPatchSetNum;
assert.equal(
element.computeDraftsStringMobile({
__path: 'myfile.txt',
@@ -954,7 +990,9 @@
}),
''
);
- element.patchRange = _1To2;
+
+ element.basePatchNum = 1 as BasePatchSetNum;
+ element.patchNum = 2 as RevisionPatchSetNum;
assert.equal(
element.computeDraftsStringMobile({
__path: 'myfile.txt',
@@ -973,10 +1011,8 @@
normalize({}, 'myfile.txt'),
];
element.changeNum = 42 as NumericChangeId;
- element.patchRange = {
- basePatchNum: PARENT,
- patchNum: 2 as RevisionPatchSetNum,
- };
+ element.basePatchNum = PARENT;
+ element.patchNum = 2 as RevisionPatchSetNum;
element.change = {
_number: 42 as NumericChangeId,
project: 'test-project',
@@ -1197,10 +1233,8 @@
normalize({}, 'myfile.txt'),
];
element.changeNum = 42 as NumericChangeId;
- element.patchRange = {
- basePatchNum: PARENT,
- patchNum: 2 as RevisionPatchSetNum,
- };
+ element.basePatchNum = PARENT;
+ element.patchNum = 2 as RevisionPatchSetNum;
element.fileCursor.setCursorAtIndex(0);
const reviewSpy = sinon.spy(element, 'reviewFile');
@@ -1263,10 +1297,8 @@
normalize({}, 'f2.txt'),
];
element.changeNum = 42 as NumericChangeId;
- element.patchRange = {
- basePatchNum: PARENT,
- patchNum: 2 as RevisionPatchSetNum,
- };
+ element.basePatchNum = PARENT;
+ element.patchNum = 2 as RevisionPatchSetNum;
await element.updateComplete;
const clickSpy = sinon.spy(element, 'handleFileListClick');
@@ -1303,10 +1335,8 @@
normalize({}, 'f2.txt'),
];
element.changeNum = 42 as NumericChangeId;
- element.patchRange = {
- basePatchNum: PARENT,
- patchNum: 2 as RevisionPatchSetNum,
- };
+ element.basePatchNum = PARENT;
+ element.patchNum = 2 as RevisionPatchSetNum;
element.editMode = true;
await element.updateComplete;
@@ -1324,10 +1354,8 @@
test('checkbox shows/hides diff inline', async () => {
element.files = [normalize({}, 'myfile.txt')];
element.changeNum = 42 as NumericChangeId;
- element.patchRange = {
- basePatchNum: PARENT,
- patchNum: 2 as RevisionPatchSetNum,
- };
+ element.basePatchNum = PARENT;
+ element.patchNum = 2 as RevisionPatchSetNum;
element.fileCursor.setCursorAtIndex(0);
sinon.stub(element, 'expandedFilesChanged');
await element.updateComplete;
@@ -1353,10 +1381,8 @@
test('diff mode correctly toggles the diffs', async () => {
element.files = [normalize({}, 'myfile.txt')];
element.changeNum = 42 as NumericChangeId;
- element.patchRange = {
- basePatchNum: PARENT,
- patchNum: 2 as RevisionPatchSetNum,
- };
+ element.basePatchNum = PARENT;
+ element.patchNum = 2 as RevisionPatchSetNum;
const updateDiffPrefSpy = sinon.spy(element, 'updateDiffPreferences');
element.fileCursor.setCursorAtIndex(0);
await element.updateComplete;
@@ -1383,10 +1409,8 @@
test('tapping row ignores links', async () => {
element.files = [normalize({}, '/COMMIT_MSG')];
element.changeNum = 42 as NumericChangeId;
- element.patchRange = {
- basePatchNum: PARENT,
- patchNum: 2 as RevisionPatchSetNum,
- };
+ element.basePatchNum = PARENT;
+ element.patchNum = 2 as RevisionPatchSetNum;
sinon.stub(element, 'expandedFilesChanged');
await element.updateComplete;
const commitMsgFile = queryAll<HTMLAnchorElement>(
@@ -1680,10 +1704,8 @@
};
element.changeNum = changeWithMultipleParents._number;
element.change = changeWithMultipleParents;
- element.patchRange = {
- basePatchNum: PARENT,
- patchNum: 1 as RevisionPatchSetNum,
- };
+ element.basePatchNum = PARENT;
+ element.patchNum = 1 as RevisionPatchSetNum;
await element.updateComplete;
await waitEventLoop();
});
@@ -1744,10 +1766,8 @@
});
test('not shown for non-Auto Merge base parents', async () => {
- element.patchRange = {
- basePatchNum: 1 as BasePatchSetNum,
- patchNum: 2 as RevisionPatchSetNum,
- };
+ element.basePatchNum = 1 as BasePatchSetNum;
+ element.patchNum = 2 as RevisionPatchSetNum;
await element.updateCleanlyMergedPaths();
await element.updateComplete;
@@ -1756,10 +1776,8 @@
});
test('not shown in edit mode', async () => {
- element.patchRange = {
- basePatchNum: 1 as BasePatchSetNum,
- patchNum: EDIT,
- };
+ element.basePatchNum = 1 as BasePatchSetNum;
+ element.patchNum = EDIT;
await element.updateCleanlyMergedPaths();
await element.updateComplete;
@@ -1776,10 +1794,8 @@
_number: 1 as NumericChangeId,
project: 'gerrit' as RepoName,
};
- element.patchRange = {
- basePatchNum: PARENT,
- patchNum: 1 as RevisionPatchSetNum,
- };
+ element.basePatchNum = PARENT;
+ element.patchNum = 1 as RevisionPatchSetNum;
const path = 'index.php';
element.editMode = false;
assert.equal(element.computeDiffURL(path), '/c/gerrit/+/1/1/index.php');
@@ -1791,10 +1807,8 @@
_number: 1 as NumericChangeId,
project: 'gerrit' as RepoName,
};
- element.patchRange = {
- basePatchNum: PARENT,
- patchNum: 1 as RevisionPatchSetNum,
- };
+ element.basePatchNum = PARENT;
+ element.patchNum = 1 as RevisionPatchSetNum;
element.editMode = false;
const path = '/COMMIT_MSG';
assert.equal(element.computeDiffURL(path), '/c/gerrit/+/1/1//COMMIT_MSG');
@@ -1806,10 +1820,8 @@
_number: 1 as NumericChangeId,
project: 'gerrit' as RepoName,
};
- element.patchRange = {
- basePatchNum: PARENT,
- patchNum: 1 as RevisionPatchSetNum,
- };
+ element.basePatchNum = PARENT;
+ element.patchNum = 1 as RevisionPatchSetNum;
element.editMode = true;
const path = 'index.php';
assert.equal(
@@ -1824,10 +1836,8 @@
_number: 1 as NumericChangeId,
project: 'gerrit' as RepoName,
};
- element.patchRange = {
- basePatchNum: PARENT,
- patchNum: 1 as RevisionPatchSetNum,
- };
+ element.basePatchNum = PARENT;
+ element.patchNum = 1 as RevisionPatchSetNum;
element.editMode = true;
const path = '/COMMIT_MSG';
assert.equal(
@@ -2113,10 +2123,8 @@
];
element.reviewed = ['/COMMIT_MSG', 'myfile.txt'];
element.changeNum = 42 as NumericChangeId;
- element.patchRange = {
- basePatchNum: PARENT,
- patchNum: 2 as RevisionPatchSetNum,
- };
+ element.basePatchNum = PARENT;
+ element.patchNum = 2 as RevisionPatchSetNum;
sinon
.stub(window, 'fetch')
.callsFake(() => Promise.resolve(new Response()));
diff --git a/polygerrit-ui/app/elements/change/gr-included-in-dialog/gr-included-in-dialog.ts b/polygerrit-ui/app/elements/change/gr-included-in-dialog/gr-included-in-dialog.ts
index fcfe209..33dfe82 100644
--- a/polygerrit-ui/app/elements/change/gr-included-in-dialog/gr-included-in-dialog.ts
+++ b/polygerrit-ui/app/elements/change/gr-included-in-dialog/gr-included-in-dialog.ts
@@ -12,6 +12,7 @@
import {LitElement, PropertyValues, html, css} from 'lit';
import {customElement, property, state} from 'lit/decorators.js';
import {BindValueChangeEvent} from '../../../types/events';
+import {fireNoBubble} from '../../../utils/event-util';
interface DisplayGroup {
title: string;
@@ -197,12 +198,7 @@
private handleCloseTap(e: Event) {
e.preventDefault();
e.stopPropagation();
- this.dispatchEvent(
- new CustomEvent('close', {
- composed: true,
- bubbles: false,
- })
- );
+ fireNoBubble(this, 'close', {});
}
}
diff --git a/polygerrit-ui/app/elements/change/gr-label-score-row/gr-label-score-row.ts b/polygerrit-ui/app/elements/change/gr-label-score-row/gr-label-score-row.ts
index 50c5caf..c669209 100644
--- a/polygerrit-ui/app/elements/change/gr-label-score-row/gr-label-score-row.ts
+++ b/polygerrit-ui/app/elements/change/gr-label-score-row/gr-label-score-row.ts
@@ -18,21 +18,20 @@
import {assertIsDefined, hasOwnProperty} from '../../../utils/common-util';
import {Label} from '../../../utils/label-util';
import {LabelNameToValuesMap} from '../../../api/rest-api';
+import {fire} from '../../../utils/event-util';
+import {LabelsChangedDetail} from '../../../api/change-reply';
declare global {
interface HTMLElementTagNameMap {
'gr-label-score-row': GrLabelScoreRow;
}
+ interface HTMLElementEventMap {
+ 'labels-changed': CustomEvent<LabelsChangedDetail>;
+ }
}
@customElement('gr-label-score-row')
export class GrLabelScoreRow extends LitElement {
- /**
- * Fired when any label is changed.
- *
- * @event labels-changed
- */
-
@query('#labelSelector')
labelSelector?: IronSelectorElement;
@@ -365,13 +364,7 @@
this.selectedValueText = selectedItem.getAttribute('title') || '';
const name = selectedItem.dataset['name'];
const value = selectedItem.dataset['value'];
- this.dispatchEvent(
- new CustomEvent('labels-changed', {
- detail: {name, value},
- bubbles: true,
- composed: true,
- })
- );
+ if (name && value) fire(this, 'labels-changed', {name, value});
};
private computePermittedLabelValues() {
diff --git a/polygerrit-ui/app/elements/change/gr-label-scores/gr-label-scores.ts b/polygerrit-ui/app/elements/change/gr-label-scores/gr-label-scores.ts
index 873e6ce..3a83fa1 100644
--- a/polygerrit-ui/app/elements/change/gr-label-scores/gr-label-scores.ts
+++ b/polygerrit-ui/app/elements/change/gr-label-scores/gr-label-scores.ts
@@ -5,7 +5,7 @@
*/
import '../gr-label-score-row/gr-label-score-row';
import '../../../styles/shared-styles';
-import {LitElement, css, html} from 'lit';
+import {LitElement, css, html, nothing} from 'lit';
import {customElement, property} from 'lit/decorators.js';
import {
ChangeInfo,
@@ -107,8 +107,7 @@
label => !this.permittedLabels || this.permittedLabels[label.name]
).length === 0
) {
- return html`<h3 class="heading-4">Trigger Votes</h3>
- <div class="permissionMessage">You don't have permission to vote</div>`;
+ return nothing;
}
return html`<h3 class="heading-4">Trigger Votes</h3>
${this.renderLabels(labels)}`;
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 a4da747..72969a7 100644
--- a/polygerrit-ui/app/elements/change/gr-message/gr-message.ts
+++ b/polygerrit-ui/app/elements/change/gr-message/gr-message.ts
@@ -48,6 +48,11 @@
import {FormattedReviewerUpdateInfo} from '../../../types/types';
import {resolve} from '../../../models/dependency';
import {createChangeUrl} from '../../../models/views/change';
+import {fire} from '../../../utils/event-util';
+import {
+ ChangeMessageDeletedEventDetail,
+ ReplyEvent,
+} from '../../../types/events';
const UPLOADED_NEW_PATCHSET_PATTERN = /Uploaded patch set (\d+)./;
const MERGED_PATCHSET_PATTERN = /(\d+) is the latest approved patch-set/;
@@ -55,6 +60,12 @@
interface HTMLElementTagNameMap {
'gr-message': GrMessage;
}
+ interface HTMLElementEventMap {
+ 'message-anchor-tap': CustomEvent<MessageAnchorTapDetail>;
+ 'change-message-deleted': CustomEvent<ChangeMessageDeletedEventDetail>;
+ // prettier-ignore
+ 'reply': ReplyEvent;
+ }
}
export interface MessageAnchorTapDetail {
@@ -70,12 +81,6 @@
*/
/**
- * Fired when the message's timestamp is tapped.
- *
- * @event message-anchor-tap
- */
-
- /**
* Fired when a change message is deleted.
*
* @event change-message-deleted
@@ -751,24 +756,13 @@
const detail: MessageAnchorTapDetail = {
id: this.message!.id,
};
- this.dispatchEvent(
- new CustomEvent('message-anchor-tap', {
- bubbles: true,
- composed: true,
- detail,
- })
- );
+ fire(this, 'message-anchor-tap', detail);
}
private handleReplyTap(e: Event) {
e.preventDefault();
- this.dispatchEvent(
- new CustomEvent('reply', {
- detail: {message: this.message},
- composed: true,
- bubbles: true,
- })
- );
+ // TODO: Fix the type casting. Might actually be a bug.
+ fire(this, 'reply', {message: this.message as ChangeMessage});
}
private handleDeleteMessage(e: Event) {
@@ -779,13 +773,10 @@
.deleteChangeCommitMessage(this.changeNum, this.message.id)
.then(() => {
this.isDeletingChangeMsg = false;
- this.dispatchEvent(
- new CustomEvent('change-message-deleted', {
- detail: {message: this.message},
- composed: true,
- bubbles: true,
- })
- );
+ // TODO: Fix the type casting. Might actually be a bug.
+ fire(this, 'change-message-deleted', {
+ message: this.message as ChangeMessage,
+ });
});
}
diff --git a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.ts b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.ts
index f3c23aa..a2ffcb8 100644
--- a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.ts
+++ b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.ts
@@ -92,7 +92,10 @@
import {pluralize} from '../../../utils/string-util';
import {
fireAlert,
- fireEvent,
+ fireError,
+ fire,
+ fireNoBubble,
+ fireNoBubbleNoCompose,
fireIronAnnounce,
fireReload,
fireServerError,
@@ -116,7 +119,11 @@
import {sharedStyles} from '../../../styles/shared-styles';
import {when} from 'lit/directives/when.js';
import {classMap} from 'lit/directives/class-map.js';
-import {ValueChangedEvent} from '../../../types/events';
+import {
+ AddReviewerEvent,
+ RemoveReviewerEvent,
+ ValueChangedEvent,
+} from '../../../types/events';
import {customElement, property, state, query} from 'lit/decorators.js';
import {subscribe} from '../../lit/subscription-controller';
import {configModelToken} from '../../../models/config/config-model';
@@ -168,43 +175,6 @@
@customElement('gr-reply-dialog')
export class GrReplyDialog extends LitElement {
- /**
- * Fired when a reply is successfully sent.
- *
- * @event send
- */
-
- /**
- * Fired when the user presses the cancel button.
- *
- * @event cancel
- */
-
- /**
- * Fires to show an alert when a send is attempted on the non-latest patch.
- *
- * @event show-alert
- */
-
- /**
- * Fires when the reply dialog believes that the server side diff drafts
- * have been updated and need to be refreshed.
- *
- * @event comment-refresh
- */
-
- /**
- * Fires when the state of the send button (enabled/disabled) changes.
- *
- * @event send-disabled-changed
- */
-
- /**
- * Fired to reload the change page.
- *
- * @event reload
- */
-
FocusTarget = FocusTarget;
private readonly reporting = getAppContext().reportingService;
@@ -723,17 +693,19 @@
// Plugins on reply-reviewers endpoint can take advantage of these
// events to add / remove reviewers
- this.addEventListener('add-reviewer', e => {
+ this.addEventListener('add-reviewer', (e: AddReviewerEvent) => {
+ const reviewer = e.detail.reviewer;
// Only support account type, see more from:
// elements/shared/gr-account-list/gr-account-list.js#addAccountItem
this.reviewersList?.addAccountItem({
- account: (e as CustomEvent).detail.reviewer,
+ account: reviewer,
count: 1,
});
});
- this.addEventListener('remove-reviewer', e => {
- this.reviewersList?.removeAccount((e as CustomEvent).detail.reviewer);
+ this.addEventListener('remove-reviewer', (e: RemoveReviewerEvent) => {
+ const reviewer = e.detail.reviewer;
+ this.reviewersList?.removeAccount(reviewer);
});
}
@@ -1286,7 +1258,7 @@
if (this.restApiService.hasPendingDiffDrafts()) {
this.savingComments = true;
this.restApiService.awaitPendingDiffDrafts().then(() => {
- fireEvent(this, 'comment-refresh');
+ fire(this, 'comment-refresh', {});
this.savingComments = false;
});
}
@@ -1476,12 +1448,7 @@
this.patchsetLevelDraftMessage = '';
this.includeComments = true;
- this.dispatchEvent(
- new CustomEvent('send', {
- composed: true,
- bubbles: false,
- })
- );
+ fireNoBubble(this, 'send', {});
fireIronAnnounce(this, 'Reply sent');
return;
})
@@ -1595,7 +1562,7 @@
onAttentionExpandedChange() {
// If the attention-detail section is expanded without dispatching this
// event, then the dialog may expand beyond the screen's bottom border.
- fireEvent(this, 'iron-resize');
+ fire(this, 'iron-resize', {});
}
computeAttentionButtonTitle(sendDisabled?: boolean) {
@@ -1864,12 +1831,7 @@
async cancel() {
assertIsDefined(this.change, 'change');
if (!this.change?.owner) throw new Error('missing required owner property');
- this.dispatchEvent(
- new CustomEvent('cancel', {
- composed: true,
- bubbles: false,
- })
- );
+ fireNoBubble(this, 'cancel', {});
await this.patchsetLevelGrComment?.save();
this.rebuildReviewerArrays();
}
@@ -1900,13 +1862,7 @@
return;
}
return this.send(this.includeComments, this.canBeStarted).catch(err => {
- this.dispatchEvent(
- new CustomEvent('show-error', {
- bubbles: true,
- composed: true,
- detail: {message: `Error submitting review ${err}`},
- })
- );
+ fireError(this, `Error submitting review ${err}`);
});
}
@@ -2083,7 +2039,7 @@
}
sendDisabledChanged() {
- this.dispatchEvent(new CustomEvent('send-disabled-changed'));
+ fireNoBubbleNoCompose(this, 'send-disabled-changed', {});
}
getReviewerSuggestionsProvider(change?: ChangeInfo | ParsedChangeInfo) {
@@ -2139,4 +2095,19 @@
interface HTMLElementTagNameMap {
'gr-reply-dialog': GrReplyDialog;
}
+ interface HTMLElementEventMap {
+ /** Fired when the user presses the cancel button. */
+ // prettier-ignore
+ 'cancel': CustomEvent<{}>;
+ /**
+ * Fires when the reply dialog believes that the server side diff drafts
+ * have been updated and need to be refreshed.
+ */
+ 'comment-refresh': CustomEvent<{}>;
+ /** Fired when a reply is successfully sent. */
+ // prettier-ignore
+ 'send': CustomEvent<{}>;
+ /** Fires when the state of the send button (enabled/disabled) changes. */
+ 'send-disabled-changed': CustomEvent<{}>;
+ }
}
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 f7b3aec..5bf6464 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
@@ -1886,7 +1886,7 @@
// Remove and add to other field.
reviewers.dispatchEvent(
- new CustomEvent('remove', {
+ new CustomEvent('remove-account', {
detail: {account: reviewer1},
composed: true,
bubbles: true,
@@ -1903,14 +1903,14 @@
})
);
ccs.dispatchEvent(
- new CustomEvent('remove', {
+ new CustomEvent('remove-account', {
detail: {account: cc1},
composed: true,
bubbles: true,
})
);
ccs.dispatchEvent(
- new CustomEvent('remove', {
+ new CustomEvent('remove-account', {
detail: {account: cc3},
composed: true,
bubbles: true,
diff --git a/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list.ts b/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list.ts
index 9408b82..db19329 100644
--- a/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list.ts
+++ b/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list.ts
@@ -23,15 +23,11 @@
import {sharedStyles} from '../../../styles/shared-styles';
import {css} from 'lit';
import {nothing} from 'lit';
+import {fire} from '../../../utils/event-util';
+import {ShowReplyDialogEvent} from '../../../types/events';
@customElement('gr-reviewer-list')
export class GrReviewerList extends LitElement {
- /**
- * Fired when the "Add reviewer..." button is tapped.
- *
- * @event show-reply-dialog
- */
-
@property({type: Object}) change?: ChangeInfo;
@property({type: Object}) account?: AccountDetailInfo;
@@ -203,22 +199,10 @@
handleAddTap(e: Event) {
e.preventDefault();
const value = {
- reviewersOnly: false,
- ccsOnly: false,
+ reviewersOnly: this.reviewersOnly,
+ ccsOnly: this.ccsOnly,
};
- if (this.reviewersOnly) {
- value.reviewersOnly = true;
- }
- if (this.ccsOnly) {
- value.ccsOnly = true;
- }
- this.dispatchEvent(
- new CustomEvent('show-reply-dialog', {
- detail: {value},
- composed: true,
- bubbles: true,
- })
- );
+ fire(this, 'show-reply-dialog', {value});
}
}
@@ -226,4 +210,8 @@
interface HTMLElementTagNameMap {
'gr-reviewer-list': GrReviewerList;
}
+ interface HTMLElementEventMap {
+ /** Fired when the "Add reviewer..." button is tapped. */
+ 'show-reply-dialog': ShowReplyDialogEvent;
+ }
}
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 66ad38c..2766114 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
@@ -10,13 +10,12 @@
import {
AccountInfo,
ChangeStatus,
- isDetailedLabelInfo,
SubmitRequirementExpressionInfo,
SubmitRequirementResultInfo,
SubmitRequirementStatus,
} from '../../../api/rest-api';
import {
- canVote,
+ canReviewerVote,
extractAssociatedLabels,
getApprovalInfo,
hasVotes,
@@ -204,7 +203,7 @@
if (requirementLabels.includes(label)) {
const labelInfo = allLabels[label];
const canSomeoneVote = (this.change?.reviewers['REVIEWER'] ?? []).some(
- reviewer => canVote(labelInfo, reviewer)
+ reviewer => canReviewerVote(labelInfo, reviewer)
);
if (hasVotes(labelInfo) || canSomeoneVote) {
labels.push(label);
@@ -280,15 +279,17 @@
labelName: string,
type: 'override' | 'submittability'
) {
+ if (!this.account) return;
+ const votes = this.change?.permitted_labels?.[labelName];
+ if (!votes || votes.length < 1) return;
+ const maxVote = Number(votes[votes.length - 1]);
+ if (maxVote <= 0) return;
+
const labels = this.change?.labels ?? {};
const labelInfo = labels[labelName];
- if (!labelInfo || !isDetailedLabelInfo(labelInfo)) return;
- if (!this.account || !canVote(labelInfo, this.account)) return;
-
const approvalInfo = getApprovalInfo(labelInfo, this.account);
- const maxVote = approvalInfo?.permitted_voting_range?.max;
- if (!maxVote || maxVote <= 0) return;
if (approvalInfo?.value === maxVote) return; // Already voted maxVote
+
return html` <div class="button quickApprove">
<gr-button
link=""
diff --git a/polygerrit-ui/app/elements/change/gr-submit-requirement-hovercard/gr-submit-requirement-hovercard_test.ts b/polygerrit-ui/app/elements/change/gr-submit-requirement-hovercard/gr-submit-requirement-hovercard_test.ts
index 4e78e8a..18f8e4c 100644
--- a/polygerrit-ui/app/elements/change/gr-submit-requirement-hovercard/gr-submit-requirement-hovercard_test.ts
+++ b/polygerrit-ui/app/elements/change/gr-submit-requirement-hovercard/gr-submit-requirement-hovercard_test.ts
@@ -253,6 +253,9 @@
const change: ParsedChangeInfo = {
...createParsedChange(),
status: ChangeStatus.NEW,
+ permitted_labels: {
+ Verified: ['-1', ' 0', '+1', '+2'],
+ },
labels: {
Verified: {
...createDetailedLabelInfo(),
@@ -351,6 +354,9 @@
const change: ParsedChangeInfo = {
...createParsedChange(),
status: ChangeStatus.NEW,
+ permitted_labels: {
+ 'Build-Cop': ['-1', ' 0', '+1', '+2'],
+ },
labels: {
'Build-Cop': {
...createDetailedLabelInfo(),
diff --git a/polygerrit-ui/app/elements/change/gr-thread-list/gr-thread-list.ts b/polygerrit-ui/app/elements/change/gr-thread-list/gr-thread-list.ts
index c09d2a2..599e38b 100644
--- a/polygerrit-ui/app/elements/change/gr-thread-list/gr-thread-list.ts
+++ b/polygerrit-ui/app/elements/change/gr-thread-list/gr-thread-list.ts
@@ -28,7 +28,11 @@
} from '../../../utils/comment-util';
import {pluralize} from '../../../utils/string-util';
import {assertIsDefined} from '../../../utils/common-util';
-import {CommentTabState, TabState} from '../../../types/events';
+import {
+ CommentTabState,
+ TabState,
+ ValueChangedEvent,
+} from '../../../types/events';
import {DropdownItem} from '../../shared/gr-dropdown-list/gr-dropdown-list';
import {GrAccountChip} from '../../shared/gr-account-chip/gr-account-chip';
import {css, html, LitElement, PropertyValues} from 'lit';
@@ -365,7 +369,7 @@
<gr-dropdown-list
id="sortDropdown"
.value=${this.sortDropdownValue}
- @value-change=${(e: CustomEvent) =>
+ @value-change=${(e: ValueChangedEvent<SortDropdownState>) =>
(this.sortDropdownValue = e.detail.value)}
.items=${this.getSortDropdownEntries()}
>
@@ -521,7 +525,7 @@
}
// private, but visible for testing
- handleCommentsDropdownValueChange(e: CustomEvent) {
+ handleCommentsDropdownValueChange(e: ValueChangedEvent<CommentTabState>) {
const value = e.detail.value;
switch (value) {
case CommentTabState.UNRESOLVED:
diff --git a/polygerrit-ui/app/elements/checks/gr-checks-results.ts b/polygerrit-ui/app/elements/checks/gr-checks-results.ts
index 5792230..f3be5c4 100644
--- a/polygerrit-ui/app/elements/checks/gr-checks-results.ts
+++ b/polygerrit-ui/app/elements/checks/gr-checks-results.ts
@@ -329,9 +329,9 @@
private computeIsExpandable() {
const hasSummary = !!this.result?.summary;
const hasMessage = !!this.result?.message;
- const hasLinks = (this.result?.links ?? []).length > 0;
+ const hasMultipleLinks = (this.result?.links ?? []).length > 1;
const hasPointers = (this.result?.codePointers ?? []).length > 0;
- return hasSummary && (hasMessage || hasLinks || hasPointers);
+ return hasSummary && (hasMessage || hasMultipleLinks || hasPointers);
}
override focus() {
diff --git a/polygerrit-ui/app/elements/checks/gr-checks-results_test.ts b/polygerrit-ui/app/elements/checks/gr-checks-results_test.ts
index 113470c..385bde7 100644
--- a/polygerrit-ui/app/elements/checks/gr-checks-results_test.ts
+++ b/polygerrit-ui/app/elements/checks/gr-checks-results_test.ts
@@ -117,6 +117,7 @@
aria-checked="false"
aria-label="Expand result row"
class="show-hide"
+ hidden
role="switch"
tabindex="0"
>
@@ -261,7 +262,6 @@
</h3>
<gr-result-row
class="FAKEErrorFinderFinderFinderFinderFinderFinderFinder"
- isexpandable
>
</gr-result-row>
<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 128a9b0a..8ba9895 100644
--- a/polygerrit-ui/app/elements/checks/gr-checks-runs.ts
+++ b/polygerrit-ui/app/elements/checks/gr-checks-runs.ts
@@ -634,7 +634,7 @@
return Object.entries(this.errorMessages).map(([plugin, message]) => {
const msg = this.collapsed
? 'Error'
- : `Error while fetching results for ${plugin}:<br />${message}`;
+ : html`Error while fetching results for ${plugin}:<br />${message}`;
return html`
<div class="error">
<div class="left">
diff --git a/polygerrit-ui/app/elements/checks/gr-checks-runs_test.ts b/polygerrit-ui/app/elements/checks/gr-checks-runs_test.ts
index 4bd3446..a858e4d 100644
--- a/polygerrit-ui/app/elements/checks/gr-checks-runs_test.ts
+++ b/polygerrit-ui/app/elements/checks/gr-checks-runs_test.ts
@@ -22,6 +22,7 @@
);
const getChecksModel = resolve(element, checksModelToken);
setAllFakeRuns(getChecksModel());
+ element.errorMessages = {'test-plugin-name': 'test-error-message'};
await element.updateComplete;
});
@@ -57,6 +58,17 @@
</gr-button>
</gr-tooltip-content>
</h2>
+ <div class="error">
+ <div class="left">
+ <gr-icon filled="" icon="error"> </gr-icon>
+ </div>
+ <div class="right">
+ <div class="message">
+ Error while fetching results for test-plugin-name: <br />
+ test-error-message
+ </div>
+ </div>
+ </div>
<input
id="filterInput"
placeholder="Filter runs by regular expression"
@@ -121,6 +133,14 @@
</gr-button>
</gr-tooltip-content>
</h2>
+ <div class="error">
+ <div class="left">
+ <gr-icon filled="" icon="error"> </gr-icon>
+ </div>
+ <div class="right">
+ <div class="message">Error</div>
+ </div>
+ </div>
<input
hidden
id="filterInput"
diff --git a/polygerrit-ui/app/elements/checks/gr-checks-util.ts b/polygerrit-ui/app/elements/checks/gr-checks-util.ts
index c7477c4..f1a3fb9 100644
--- a/polygerrit-ui/app/elements/checks/gr-checks-util.ts
+++ b/polygerrit-ui/app/elements/checks/gr-checks-util.ts
@@ -9,6 +9,7 @@
AttemptChoice,
LATEST_ATTEMPT,
} from '../../models/checks/checks-util';
+import {fire} from '../../utils/event-util';
export interface RunSelectedEventDetail {
checkName?: string;
@@ -23,13 +24,7 @@
}
export function fireRunSelected(target: EventTarget, checkName: string) {
- target.dispatchEvent(
- new CustomEvent('run-selected', {
- detail: {reset: false, checkName},
- composed: true,
- bubbles: true,
- })
- );
+ fire(target, 'run-selected', {checkName});
}
export function isAttemptSelected(
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 b46d2b9..98dd574 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
@@ -8,7 +8,7 @@
import {getUserName} from '../../../utils/display-name-util';
import {AccountInfo, ServerInfo} from '../../../types/common';
import {getAppContext} from '../../../services/app-context';
-import {fireEvent} from '../../../utils/event-util';
+import {fire} from '../../../utils/event-util';
import {
DropdownContent,
DropdownLink,
@@ -23,6 +23,9 @@
interface HTMLElementTagNameMap {
'gr-account-dropdown': GrAccountDropdown;
}
+ interface HTMLElementEventMap {
+ 'show-keyboard-shortcuts': CustomEvent<{}>;
+ }
}
@customElement('gr-account-dropdown')
@@ -136,7 +139,7 @@
}
_handleShortcutsTap() {
- fireEvent(this, 'show-keyboard-shortcuts');
+ fire(this, 'show-keyboard-shortcuts', {});
}
private readonly handleLocationChange = () => {
diff --git a/polygerrit-ui/app/elements/core/gr-error-dialog/gr-error-dialog.ts b/polygerrit-ui/app/elements/core/gr-error-dialog/gr-error-dialog.ts
index 461781e..5510aa3 100644
--- a/polygerrit-ui/app/elements/core/gr-error-dialog/gr-error-dialog.ts
+++ b/polygerrit-ui/app/elements/core/gr-error-dialog/gr-error-dialog.ts
@@ -7,11 +7,16 @@
import {sharedStyles} from '../../../styles/shared-styles';
import {LitElement, html, css} from 'lit';
import {customElement, property} from 'lit/decorators.js';
+import {fireNoBubbleNoCompose} from '../../../utils/event-util';
declare global {
interface HTMLElementTagNameMap {
'gr-error-dialog': GrErrorDialog;
}
+ interface HTMLElementEventMap {
+ // prettier-ignore
+ 'dismiss': CustomEvent<{}>;
+ }
}
@customElement('gr-error-dialog')
@@ -83,6 +88,6 @@
}
private handleConfirm() {
- this.dispatchEvent(new CustomEvent('dismiss'));
+ fireNoBubbleNoCompose(this, 'dismiss', {});
}
}
diff --git a/polygerrit-ui/app/elements/core/gr-error-manager/gr-error-manager.ts b/polygerrit-ui/app/elements/core/gr-error-manager/gr-error-manager.ts
index 1176ce3..e8675bc 100644
--- a/polygerrit-ui/app/elements/core/gr-error-manager/gr-error-manager.ts
+++ b/polygerrit-ui/app/elements/core/gr-error-manager/gr-error-manager.ts
@@ -14,7 +14,6 @@
import {AccountId} from '../../../types/common';
import {
AuthErrorEvent,
- EventType,
NetworkErrorEvent,
ServerErrorEvent,
ShowAlertEventDetail,
@@ -124,9 +123,9 @@
override connectedCallback() {
super.connectedCallback();
- document.addEventListener(EventType.SERVER_ERROR, this.handleServerError);
- document.addEventListener(EventType.NETWORK_ERROR, this.handleNetworkError);
- document.addEventListener(EventType.SHOW_ALERT, this.handleShowAlert);
+ document.addEventListener('server-error', this.handleServerError);
+ document.addEventListener('network-error', this.handleNetworkError);
+ document.addEventListener('show-alert', this.handleShowAlert);
document.addEventListener('hide-alert', this.hideAlert);
document.addEventListener('show-error', this.handleShowErrorDialog);
document.addEventListener('visibilitychange', this.handleVisibilityChange);
@@ -140,15 +139,9 @@
override disconnectedCallback() {
this.clearHideAlertHandle();
- document.removeEventListener(
- EventType.SERVER_ERROR,
- this.handleServerError
- );
- document.removeEventListener(
- EventType.NETWORK_ERROR,
- this.handleNetworkError
- );
- document.removeEventListener(EventType.SHOW_ALERT, this.handleShowAlert);
+ document.removeEventListener('server-error', this.handleServerError);
+ document.removeEventListener('network-error', this.handleNetworkError);
+ document.removeEventListener('show-alert', this.handleShowAlert);
document.removeEventListener('hide-alert', this.hideAlert);
document.removeEventListener('show-error', this.handleShowErrorDialog);
document.removeEventListener(
@@ -358,7 +351,7 @@
el.show(text, actionText, actionCallback);
this.alertElement = el;
fireIronAnnounce(this, `Alert: ${text}`);
- this.reporting.reportInteraction(EventType.SHOW_ALERT, {text});
+ this.reporting.reportInteraction('show-alert', {text});
}
private readonly hideAlert = () => {
diff --git a/polygerrit-ui/app/elements/core/gr-error-manager/gr-error-manager_test.ts b/polygerrit-ui/app/elements/core/gr-error-manager/gr-error-manager_test.ts
index f4ee5ed..fff69ef 100644
--- a/polygerrit-ui/app/elements/core/gr-error-manager/gr-error-manager_test.ts
+++ b/polygerrit-ui/app/elements/core/gr-error-manager/gr-error-manager_test.ts
@@ -24,7 +24,6 @@
import {waitUntil} from '../../../test/test-utils';
import {fixture, assert} from '@open-wc/testing';
import {html} from 'lit';
-import {EventType} from '../../../types/events';
import {testResolver} from '../../../test/common-test-setup';
import {authServiceToken} from '../../../services/gr-auth/gr-auth';
@@ -391,7 +390,7 @@
// fake an alert
element.dispatchEvent(
- new CustomEvent(EventType.SHOW_ALERT, {
+ new CustomEvent('show-alert', {
detail: {message: 'test reload', action: 'reload'},
composed: true,
bubbles: true,
@@ -439,7 +438,7 @@
// fake an alert
element.dispatchEvent(
- new CustomEvent(EventType.SHOW_ALERT, {
+ new CustomEvent('show-alert', {
detail: {message: 'test reload', action: 'reload'},
composed: true,
bubbles: true,
@@ -452,7 +451,7 @@
// new alert
element.dispatchEvent(
- new CustomEvent(EventType.SHOW_ALERT, {
+ new CustomEvent('show-alert', {
detail: {message: 'second-test', action: 'reload'},
composed: true,
bubbles: true,
@@ -498,7 +497,7 @@
// fake an alert
element.dispatchEvent(
- new CustomEvent(EventType.SHOW_ALERT, {
+ new CustomEvent('show-alert', {
detail: {
message: 'test-alert',
action: 'reload',
@@ -519,7 +518,7 @@
const alertObj = {message: 'foo'};
const showAlertStub = sinon.stub(element, '_showAlert');
element.dispatchEvent(
- new CustomEvent(EventType.SHOW_ALERT, {
+ new CustomEvent('show-alert', {
detail: alertObj,
composed: true,
bubbles: true,
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 45ba33b..a359072 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
@@ -16,6 +16,7 @@
ShortcutViewListener,
} from '../../../services/shortcuts/shortcuts-service';
import {resolve} from '../../../models/dependency';
+import {fireNoBubble} from '../../../utils/event-util';
declare global {
interface HTMLElementTagNameMap {
@@ -162,12 +163,7 @@
private handleCloseTap(e: MouseEvent) {
e.preventDefault();
e.stopPropagation();
- this.dispatchEvent(
- new CustomEvent('close', {
- composed: true,
- bubbles: false,
- })
- );
+ fireNoBubble(this, 'close', {});
}
onDirectoryUpdated(directory?: Map<ShortcutSection, SectionView>) {
diff --git a/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header.ts b/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header.ts
index 833a91a..beb843a 100644
--- a/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header.ts
+++ b/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header.ts
@@ -25,7 +25,7 @@
import {sharedStyles} from '../../../styles/shared-styles';
import {LitElement, PropertyValues, html, css} from 'lit';
import {customElement, property, state} from 'lit/decorators.js';
-import {fireEvent} from '../../../utils/event-util';
+import {fire} from '../../../utils/event-util';
import {resolve} from '../../../models/dependency';
import {configModelToken} from '../../../models/config/config-model';
import {userModelToken} from '../../../models/user/user-model';
@@ -97,6 +97,9 @@
interface HTMLElementTagNameMap {
'gr-main-header': GrMainHeader;
}
+ interface HTMLElementEventMap {
+ 'mobile-search': CustomEvent<{}>;
+ }
}
@customElement('gr-main-header')
@@ -641,6 +644,6 @@
private onMobileSearchTap(e: Event) {
e.preventDefault();
e.stopPropagation();
- fireEvent(this, 'mobile-search');
+ fire(this, 'mobile-search', {});
}
}
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 fe90471..1b377e8 100644
--- a/polygerrit-ui/app/elements/core/gr-router/gr-router.ts
+++ b/polygerrit-ui/app/elements/core/gr-router/gr-router.ts
@@ -26,7 +26,7 @@
import {AppElement, AppElementParams} from '../../gr-app-types';
import {LocationChangeEventDetail} from '../../../types/events';
import {GerritView, RouterModel} from '../../../services/router/router-model';
-import {fireAlert, firePageError} from '../../../utils/event-util';
+import {fire, fireAlert, firePageError} from '../../../utils/event-util';
import {windowLocationReload} from '../../../utils/dom-util';
import {
encodeURL,
@@ -555,13 +555,7 @@
hash: window.location.hash,
pathname: window.location.pathname,
};
- document.dispatchEvent(
- new CustomEvent('location-change', {
- detail,
- composed: true,
- bubbles: true,
- })
- );
+ fire(document, 'location-change', detail);
}
_testOnly_startRouter() {
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 314e126..406755f 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
@@ -7,6 +7,7 @@
import '../../shared/gr-icon/gr-icon';
import {ServerInfo} from '../../../types/common';
import {
+ AutocompleteCommitEvent,
AutocompleteQuery,
AutocompleteSuggestion,
GrAutocomplete,
@@ -25,6 +26,8 @@
import {configModelToken} from '../../../models/config/config-model';
import {resolve} from '../../../models/dependency';
import {subscribe} from '../../lit/subscription-controller';
+import {ValueChangedEvent} from '../../../types/events';
+import {fireNoBubbleNoCompose} from '../../../utils/event-util';
// Possible static search options for auto complete, without negations.
const SEARCH_OPERATORS: ReadonlyArray<string> = [
@@ -227,10 +230,10 @@
.threshold=${this.threshold}
tab-complete
.verticalOffset=${30}
- @commit=${(e: Event) => {
+ @commit=${(e: AutocompleteCommitEvent) => {
this.handleInputCommit(e);
}}
- @text-changed=${(e: CustomEvent) => {
+ @text-changed=${(e: ValueChangedEvent) => {
this.handleSearchTextChanged(e);
}}
>
@@ -285,7 +288,7 @@
return `${baseUrl}/user-search.html`;
}
- private handleInputCommit(e: Event) {
+ private handleInputCommit(e: AutocompleteCommitEvent) {
this.preventDefaultAndNavigateToInputVal(e);
}
@@ -295,7 +298,7 @@
* - e.target is the gr-autocomplete widget (#searchInput)
* - e.target is the input element wrapped within #searchInput
*/
- private preventDefaultAndNavigateToInputVal(e: Event) {
+ private preventDefaultAndNavigateToInputVal(e: AutocompleteCommitEvent) {
e.preventDefault();
if (!this.inputVal) return;
const trimmedInput = this.inputVal.trim();
@@ -309,11 +312,7 @@
const detail: SearchBarHandleSearchDetail = {
inputVal: this.inputVal,
};
- this.dispatchEvent(
- new CustomEvent('handle-search', {
- detail,
- })
- );
+ fireNoBubbleNoCompose(this, 'handle-search', detail);
}
}
@@ -425,7 +424,7 @@
this.searchInput.selectAll();
}
- private handleSearchTextChanged(e: CustomEvent) {
+ private handleSearchTextChanged(e: ValueChangedEvent) {
this.inputVal = e.detail.value;
}
}
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 1608c22..9d29cea 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
@@ -36,7 +36,6 @@
import {GrSyntaxLayerWorker} from '../../../embed/diff/gr-syntax-layer/gr-syntax-layer-worker';
import {highlightServiceToken} from '../../../services/highlight/highlight-service';
import {anyLineTooLong} from '../../../embed/diff/gr-diff/gr-diff-utils';
-import {changeModelToken} from '../../../models/change/change-model';
import {fireReload} from '../../../utils/event-util';
interface FilePreview {
@@ -92,9 +91,6 @@
diffPrefs?: DiffPreferencesInfo;
@state()
- isOwner = false;
-
- @state()
onCloseFixPreviewCallbacks: ((fixapplied: boolean) => void)[] = [];
private readonly restApiService = getAppContext().restApiService;
@@ -103,8 +99,6 @@
private readonly getNavigation = resolve(this, navigationToken);
- private readonly getChangeModel = resolve(this, changeModelToken);
-
private readonly syntaxLayer = new GrSyntaxLayerWorker(
resolve(this, highlightServiceToken),
() => getAppContext().reportingService
@@ -114,11 +108,6 @@
super();
subscribe(
this,
- () => this.getChangeModel().isOwner$,
- x => (this.isOwner = x)
- );
- subscribe(
- this,
() => this.getUserModel().preferences$,
preferences => {
const layers: DiffLayer[] = [this.syntaxLayer];
@@ -341,7 +330,6 @@
private computeTooltip() {
if (!this.change || !this.patchNum) return '';
- if (!this.isOwner) return 'Fix can only be applied by author';
const latestPatchNum =
this.change.revisions[this.change.current_revision]._number;
return latestPatchNum !== this.patchNum
@@ -351,7 +339,6 @@
private computeDisableApplyFixButton() {
if (!this.change || !this.patchNum) return true;
- if (!this.isOwner) return true;
const latestPatchNum =
this.change.revisions[this.change.current_revision]._number;
return this.patchNum !== latestPatchNum || this.isApplyFixLoading;
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 9284fb2..b317b97 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
@@ -17,7 +17,7 @@
} from '../../../test/test-data-generators';
import {createDefaultDiffPrefs} from '../../../constants/constants';
import {DiffInfo} from '../../../types/diff';
-import {EventType, OpenFixPreviewEventDetail} from '../../../types/events';
+import {OpenFixPreviewEventDetail} from '../../../types/events';
import {GrButton} from '../../shared/gr-button/gr-button';
import {fixture, html, assert} from '@open-wc/testing';
import {SinonStub} from 'sinon';
@@ -51,7 +51,7 @@
async function open(detail: OpenFixPreviewEventDetail) {
element.open(
- new CustomEvent<OpenFixPreviewEventDetail>(EventType.OPEN_FIX_PREVIEW, {
+ new CustomEvent<OpenFixPreviewEventDetail>('open-fix-preview', {
detail,
})
);
@@ -71,7 +71,6 @@
element.changeNum = change._number;
element.patchNum = change.revisions[change.current_revision]._number;
element.change = change;
- element.isOwner = true;
element.diffPrefs = {
...createDefaultDiffPrefs(),
font_size: 12,
@@ -161,22 +160,8 @@
assert.equal(button.getAttribute('title'), '');
});
- test('apply fix button is disabled for non-author', async () => {
- element.isOwner = false;
- await element.updateComplete;
- await open(TWO_FIXES);
- assert.equal(element.currentFix!.fix_id, 'fix_1');
- assert.equal(element.currentPreviews.length, 2);
- const button = getConfirmButton();
- assert.isTrue(button.hasAttribute('disabled'));
- assert.equal(
- button.getAttribute('title'),
- 'Fix can only be applied by author'
- );
- });
-
test('apply fix button is disabled on older patchset', async () => {
- element.change = {
+ element.change = element.change = {
...createParsedChange(),
revisions: createRevisions(2),
current_revision: getCurrentRevision(0),
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 fd0e5d2..43a6819 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
@@ -58,9 +58,8 @@
firePageError,
fireAlert,
fireServerError,
- fireEvent,
- waitForEventOnce,
fire,
+ waitForEventOnce,
} from '../../../utils/event-util';
import {assertIsDefined} from '../../../utils/common-util';
import {DiffContextExpandedEventDetail} from '../../../embed/diff/gr-diff-builder/gr-diff-builder';
@@ -68,7 +67,11 @@
import {Timing, Interaction} from '../../../constants/reporting';
import {ChangeComments} from '../gr-comment-api/gr-comment-api';
import {Subscription} from 'rxjs';
-import {DisplayLine, RenderPreferences} from '../../../api/diff';
+import {
+ DisplayLine,
+ LineSelectedEventDetail,
+ RenderPreferences,
+} from '../../../api/diff';
import {resolve} from '../../../models/dependency';
import {browserModelToken} from '../../../models/browser/browser-model';
import {commentsModelToken} from '../../../models/comments/comments-model';
@@ -119,8 +122,8 @@
declare global {
interface HTMLElementEventMap {
- /* prettier-ignore */
- 'render': CustomEvent;
+ // prettier-ignore
+ 'render': CustomEvent<{}>;
'diff-context-expanded': CustomEvent<DiffContextExpandedEventDetail>;
'create-comment': CustomEvent<CreateCommentEventDetail>;
'is-blame-loaded-changed': ValueChangedEvent<boolean>;
@@ -129,9 +132,9 @@
'files-weblinks-changed': ValueChangedEvent<FilesWebLinks | undefined>;
'is-image-diff-changed': ValueChangedEvent<boolean>;
// Fired when the user selects a line (See gr-diff).
- 'line-selected': CustomEvent;
+ 'line-selected': CustomEvent<LineSelectedEventDetail>;
// Fired if being logged in is required.
- 'show-auth-required': void;
+ 'show-auth-required': CustomEvent<{}>;
}
}
@@ -1238,7 +1241,7 @@
private canCommentOnPatchSetNum(patchNum: PatchSetNum) {
if (!this.loggedIn) {
- fireEvent(this, 'show-auth-required');
+ fire(this, 'show-auth-required', {});
return false;
}
if (!this.patchRange) {
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host_test.ts b/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host_test.ts
index 7a32c27..141a24d 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host_test.ts
+++ b/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host_test.ts
@@ -51,7 +51,6 @@
import {GrCommentThread} from '../../shared/gr-comment-thread/gr-comment-thread';
import {assertIsDefined} from '../../../utils/common-util';
import {fixture, html, assert} from '@open-wc/testing';
-import {EventType} from '../../../types/events';
import {testResolver} from '../../../test/common-test-setup';
import {userModelToken, UserModel} from '../../../models/user/user-model';
import {pluginLoaderToken} from '../../shared/gr-js-api-interface/gr-plugin-loader';
@@ -662,7 +661,7 @@
test('loadBlame', async () => {
const mockBlame: BlameInfo[] = [createBlame()];
const showAlertStub = sinon.stub();
- element.addEventListener(EventType.SHOW_ALERT, showAlertStub);
+ element.addEventListener('show-alert', showAlertStub);
const getBlameStub = stubRestApi('getBlame').returns(
Promise.resolve(mockBlame)
);
@@ -694,7 +693,7 @@
const mockBlame: BlameInfo[] = [];
const showAlertStub = sinon.stub();
const isBlameLoadedStub = sinon.stub();
- element.addEventListener(EventType.SHOW_ALERT, showAlertStub);
+ element.addEventListener('show-alert', showAlertStub);
element.addEventListener('is-blame-loaded-changed', isBlameLoadedStub);
stubRestApi('getBlame').returns(Promise.resolve(mockBlame));
const changeNum = 42 as NumericChangeId;
@@ -1328,7 +1327,7 @@
test('cannot create thread on an edit', () => {
const alertSpy = sinon.spy();
- element.addEventListener(EventType.SHOW_ALERT, alertSpy);
+ element.addEventListener('show-alert', alertSpy);
const diffSide = Side.RIGHT;
element.patchRange = {
@@ -1356,7 +1355,7 @@
test('cannot create thread on an edit base', () => {
const alertSpy = sinon.spy();
- element.addEventListener(EventType.SHOW_ALERT, alertSpy);
+ element.addEventListener('show-alert', alertSpy);
const diffSide = Side.LEFT;
element.patchRange = {
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-preferences-dialog/gr-diff-preferences-dialog.ts b/polygerrit-ui/app/elements/diff/gr-diff-preferences-dialog/gr-diff-preferences-dialog.ts
index 531d2ae..d580127 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-preferences-dialog/gr-diff-preferences-dialog.ts
+++ b/polygerrit-ui/app/elements/diff/gr-diff-preferences-dialog/gr-diff-preferences-dialog.ts
@@ -12,6 +12,7 @@
import {customElement, query, state} from 'lit/decorators.js';
import {ValueChangedEvent} from '../../../types/events';
import {modalStyles} from '../../../styles/gr-modal-styles';
+import {fireNoBubble} from '../../../utils/event-util';
@customElement('gr-diff-preferences-dialog')
export class GrDiffPreferencesDialog extends LitElement {
@@ -120,12 +121,7 @@
assertIsDefined(this.diffPreferences, 'diffPreferences');
assertIsDefined(this.diffPrefsModal, 'diffPrefsModal');
await this.diffPreferences.save();
- this.dispatchEvent(
- new CustomEvent('reload-diff-preference', {
- composed: true,
- bubbles: false,
- })
- );
+ fireNoBubble(this, 'reload-diff-preference', {});
this.diffPrefsModal.close();
}
@@ -140,4 +136,7 @@
interface HTMLElementTagNameMap {
'gr-diff-preferences-dialog': GrDiffPreferencesDialog;
}
+ interface HTMLElementEventMap {
+ 'reload-diff-preference': CustomEvent<{}>;
+ }
}
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 19186ec..14eb505 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
@@ -33,6 +33,7 @@
DropdownItem,
GrDropdownList,
} from '../../shared/gr-dropdown-list/gr-dropdown-list';
+import {CommentAnchorTapEventDetail} from '../../shared/gr-comment/gr-comment';
import {ChangeComments} from '../../diff/gr-comment-api/gr-comment-api';
import {
BasePatchSetNum,
@@ -48,13 +49,16 @@
} from '../../../types/common';
import {DiffInfo, DiffPreferencesInfo} from '../../../types/diff';
import {FileRange, ParsedChangeInfo} from '../../../types/types';
-import {FilesWebLinks} from '../gr-patch-range-select/gr-patch-range-select';
+import {
+ FilesWebLinks,
+ PatchRangeChangeEvent,
+} from '../gr-patch-range-select/gr-patch-range-select';
import {GrDiffCursor} from '../../../embed/diff/gr-diff-cursor/gr-diff-cursor';
import {CommentSide, DiffViewMode, Side} from '../../../constants/constants';
import {GrApplyFixDialog} from '../gr-apply-fix-dialog/gr-apply-fix-dialog';
import {CommentMap} from '../../../utils/comment-util';
import {OpenFixPreviewEvent, ValueChangedEvent} from '../../../types/events';
-import {fireAlert, fireEvent, fireTitleChange} from '../../../utils/event-util';
+import {fireAlert, fire, fireTitleChange} from '../../../utils/event-util';
import {assertIsDefined, queryAndAssert} from '../../../utils/common-util';
import {toggleClass, whenVisible} from '../../../utils/dom-util';
import {CursorMoveResult} from '../../../api/core';
@@ -66,7 +70,7 @@
ShortcutSection,
shortcutsServiceToken,
} from '../../../services/shortcuts/shortcuts-service';
-import {DisplayLine} from '../../../api/diff';
+import {DisplayLine, LineSelectedEventDetail} from '../../../api/diff';
import {GrDownloadDialog} from '../../change/gr-download-dialog/gr-download-dialog';
import {commentsModelToken} from '../../../models/comments/comments-model';
import {changeModelToken} from '../../../models/change/change-model';
@@ -768,7 +772,7 @@
.path=${this.path}
.projectName=${this.change?.project}
@is-blame-loaded-changed=${this.onIsBlameLoadedChanged}
- @comment-anchor-tap=${this.onLineSelected}
+ @comment-anchor-tap=${this.onCommentAnchorTap}
@line-selected=${this.onLineSelected}
@diff-changed=${this.onDiffChanged}
@edit-weblinks-changed=${this.onEditWeblinksChanged}
@@ -1193,7 +1197,7 @@
// Similar to gr-change-view.handleOpenReplyDialog
private handleOpenReplyDialog() {
if (!this.loggedIn) {
- fireEvent(this, 'show-auth-required');
+ fire(this, 'show-auth-required', {});
return;
}
this.getChangeModel().navigateToChange(true);
@@ -1443,7 +1447,7 @@
}
// Private but used in tests.
- handlePatchChange(e: CustomEvent) {
+ handlePatchChange(e: PatchRangeChangeEvent) {
if (!this.path) return;
if (!this.patchNum) return;
@@ -1466,16 +1470,23 @@
}
// Private but used in tests.
- onLineSelected(e: CustomEvent) {
- // for on-comment-anchor-tap side can be PARENT/REVISIONS
- // for on-line-selected side can be left/right
+ onCommentAnchorTap(e: CustomEvent<CommentAnchorTapEventDetail>) {
+ const lineNumber = e.detail.number;
+ if (!Number.isInteger(lineNumber)) return;
this.updateUrlToDiffUrl(
- e.detail.number,
- e.detail.side === Side.LEFT || e.detail.side === CommentSide.PARENT
+ lineNumber as number,
+ e.detail.side === CommentSide.PARENT
);
}
// Private but used in tests.
+ onLineSelected(e: CustomEvent<LineSelectedEventDetail>) {
+ const lineNumber = e.detail.number;
+ if (!Number.isInteger(lineNumber)) return;
+ this.updateUrlToDiffUrl(lineNumber as number, e.detail.side === Side.LEFT);
+ }
+
+ // Private but used in tests.
computeDownloadDropdownLinks() {
if (!this.change?.project) return [];
if (!this.changeNum) return [];
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view_test.ts b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view_test.ts
index 6507046..e7e1bc8 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view_test.ts
+++ b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view_test.ts
@@ -60,7 +60,6 @@
import {assertIsDefined} from '../../../utils/common-util';
import {GrDiffModeSelector} from '../../../embed/diff/gr-diff-mode-selector/gr-diff-mode-selector';
import {fixture, html, assert} from '@open-wc/testing';
-import {EventType} from '../../../types/events';
import {GrButton} from '../../shared/gr-button/gr-button';
import {testResolver} from '../../../test/common-test-setup';
import {UserModel, userModelToken} from '../../../models/user/user-model';
@@ -1698,10 +1697,7 @@
pressKey(element, 'n');
assert.isTrue(moveToNextChunkStub.called);
- assert.equal(
- dispatchEventStub.lastCall.args[0].type,
- EventType.SHOW_ALERT
- );
+ assert.equal(dispatchEventStub.lastCall.args[0].type, 'show-alert');
assert.isFalse(navToFileStub.called);
});
@@ -1741,10 +1737,7 @@
pressKey(element, 'p');
assert.isTrue(moveToPreviousChunkStub.called);
- assert.equal(
- dispatchEventStub.lastCall.args[0].type,
- EventType.SHOW_ALERT
- );
+ assert.equal(dispatchEventStub.lastCall.args[0].type, 'show-alert');
assert.isFalse(navToFileStub.called);
});
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 325798c..01c007b 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
@@ -47,6 +47,7 @@
import {GeneratedWebLink} from '../../../utils/weblink-util';
import {changeModelToken} from '../../../models/change/change-model';
import {changeViewModelToken} from '../../../models/views/change';
+import {fireNoBubbleNoCompose} from '../../../utils/event-util';
// Maximum length for patch set descriptions.
const PATCH_DESC_MAX_LENGTH = 500;
@@ -56,7 +57,7 @@
}
export interface PatchRangeChangeDetail {
- patchNum?: PatchSetNum;
+ patchNum?: RevisionPatchSetNum;
basePatchNum?: BasePatchSetNum;
}
@@ -73,6 +74,12 @@
}
}
+declare global {
+ interface HTMLElementEventMap {
+ 'patch-range-change': PatchRangeChangeEvent;
+ }
+}
+
/**
* Fired when the patch range changes
*
@@ -463,7 +470,9 @@
basePatchNum: this.basePatchNum,
};
const target = e.target;
- const patchSetValue = convertToPatchSetNum(e.detail.value)!;
+ const patchSetValue = convertToPatchSetNum(
+ e.detail.value
+ ) as RevisionPatchSetNum;
const latestPatchNum = computeLatestPatchNum(this.availablePatches);
if (target === this.patchNumDropdown) {
if (detail.patchNum === patchSetValue) return;
@@ -488,8 +497,6 @@
detail.basePatchNum = patchSetValue as BasePatchSetNum;
}
- this.dispatchEvent(
- new CustomEvent('patch-range-change', {detail, bubbles: false})
- );
+ fireNoBubbleNoCompose(this, 'patch-range-change', detail);
}
}
diff --git a/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select_test.ts b/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select_test.ts
index 584fcd7..3813946 100644
--- a/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select_test.ts
+++ b/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select_test.ts
@@ -469,10 +469,10 @@
await element.updateComplete;
const stub = stubReporting('reportInteraction');
- fire(element.patchNumDropdown!, 'value-change', {value: '1'});
+ fire(element.patchNumDropdown, 'value-change', {value: '1'});
assert.isFalse(stub.called);
- fire(element.patchNumDropdown!, 'value-change', {value: '2'});
+ fire(element.patchNumDropdown, 'value-change', {value: '2'});
assert.isTrue(stub.called);
});
});
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 7229c63..25d3cf4 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
@@ -6,11 +6,16 @@
import {sharedStyles} from '../../../styles/shared-styles';
import {LitElement, css, html} from 'lit';
import {customElement, property} from 'lit/decorators.js';
+import {fire} from '../../../utils/event-util';
+import {ValueChangedEvent} from '../../../types/events';
declare global {
interface HTMLElementTagNameMap {
'gr-default-editor': GrDefaultEditor;
}
+ interface HTMLElementEventMap {
+ 'content-change': ValueChangedEvent;
+ }
}
@customElement('gr-default-editor')
@@ -56,12 +61,7 @@
}
_handleTextareaInput(e: Event) {
- this.dispatchEvent(
- new CustomEvent('content-change', {
- detail: {value: (e.target as HTMLTextAreaElement).value},
- bubbles: true,
- composed: true,
- })
- );
+ const value = (e.target as HTMLTextAreaElement).value;
+ fire(this, 'content-change', {value});
}
}
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 c442aa6..751b64d 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
@@ -6,8 +6,11 @@
import '../../shared/gr-dropdown/gr-dropdown';
import {GrEditConstants} from '../gr-edit-constants';
import {sharedStyles} from '../../../styles/shared-styles';
+import {FileActionTapEvent} from '../../../types/events';
import {LitElement, css, html} from 'lit';
import {customElement, property} from 'lit/decorators.js';
+import {DropdownLink} from '../../shared/gr-dropdown/gr-dropdown';
+import {fire} from '../../../utils/event-util';
interface EditAction {
label: string;
@@ -16,12 +19,6 @@
@customElement('gr-edit-file-controls')
export class GrEditFileControls extends LitElement {
- /**
- * Fired when an action in the overflow menu is tapped.
- *
- * @event file-action-tap
- */
-
@property({type: String})
filePath?: string;
@@ -64,23 +61,20 @@
>`;
}
- _handleActionTap(e: CustomEvent) {
+ _handleActionTap(e: CustomEvent<DropdownLink>) {
e.preventDefault();
e.stopPropagation();
- this._dispatchFileAction(e.detail.id, this.filePath);
+ const actionId = e.detail.id;
+ if (!actionId) return;
+ if (!this.filePath) return;
+ this._dispatchFileAction(actionId, this.filePath);
}
- _dispatchFileAction(action: EditAction, path?: string) {
- this.dispatchEvent(
- new CustomEvent('file-action-tap', {
- detail: {action, path},
- bubbles: true,
- composed: true,
- })
- );
+ _dispatchFileAction(action: string, path: string) {
+ fire(this, 'file-action-tap', {action, path});
}
- _computeFileActions(actions: EditAction[]) {
+ _computeFileActions(actions: EditAction[]): DropdownLink[] {
// TODO(kaspern): conditionally disable some actions based on file status.
return actions.map(action => {
return {
@@ -95,4 +89,7 @@
interface HTMLElementTagNameMap {
'gr-edit-file-controls': GrEditFileControls;
}
+ interface HTMLElementEventMap {
+ 'file-action-tap': FileActionTapEvent;
+ }
}
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 acc4c9e..12b19c7 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
@@ -13,7 +13,7 @@
import {
EditPreferencesInfo,
Base64FileContent,
- PatchSetNumber,
+ RevisionPatchSetNum,
} from '../../../types/common';
import {ParsedChangeInfo} from '../../../types/types';
import {HttpMethod, NotifyType} from '../../../constants/constants';
@@ -26,7 +26,7 @@
import {Modifier} from '../../../utils/dom-util';
import {sharedStyles} from '../../../styles/shared-styles';
import {LitElement, PropertyValues, html, css, nothing} from 'lit';
-import {customElement, property, state} from 'lit/decorators.js';
+import {customElement, state} from 'lit/decorators.js';
import {subscribe} from '../../lit/subscription-controller';
import {resolve} from '../../../models/dependency';
import {changeModelToken} from '../../../models/change/change-model';
@@ -63,8 +63,7 @@
* @event show-alert
*/
- @property({type: Object})
- viewState?: ChangeViewState;
+ @state() viewState?: ChangeViewState;
// private but used in test
@state() change?: ParsedChangeInfo;
@@ -87,7 +86,7 @@
@state() private editPrefs?: EditPreferencesInfo;
// private but used in test
- @state() latestPatchsetNumber?: PatchSetNumber;
+ @state() latestPatchsetNumber?: RevisionPatchSetNum;
private readonly restApiService = getAppContext().restApiService;
@@ -130,7 +129,7 @@
);
subscribe(
this,
- () => this.getChangeModel().latestPatchNum$,
+ () => this.getChangeModel().latestPatchNumWithEdit$,
x => (this.latestPatchsetNumber = x)
);
this.shortcuts.addLocal({key: 's', modifiers: [Modifier.CTRL_KEY]}, () =>
diff --git a/polygerrit-ui/app/elements/edit/gr-editor-view/gr-editor-view_test.ts b/polygerrit-ui/app/elements/edit/gr-editor-view/gr-editor-view_test.ts
index 1a4879d..0a36a05 100644
--- a/polygerrit-ui/app/elements/edit/gr-editor-view/gr-editor-view_test.ts
+++ b/polygerrit-ui/app/elements/edit/gr-editor-view/gr-editor-view_test.ts
@@ -17,7 +17,6 @@
import {
EDIT,
NumericChangeId,
- PatchSetNumber,
RevisionPatchSetNum,
} from '../../../types/common';
import {
@@ -28,7 +27,6 @@
import {GrDefaultEditor} from '../gr-default-editor/gr-default-editor';
import {GrButton} from '../../shared/gr-button/gr-button';
import {fixture, html, assert} from '@open-wc/testing';
-import {EventType} from '../../../types/events';
import {Modifier} from '../../../utils/dom-util';
import {testResolver} from '../../../test/common-test-setup';
import {storageServiceToken} from '../../../services/storage/gr-storage_impl';
@@ -51,9 +49,9 @@
navigateStub = sinon.stub(element, 'viewEditInChangeView');
element.viewState = {
...createEditViewState(),
- patchNum: 1 as PatchSetNumber,
+ patchNum: 1 as RevisionPatchSetNum,
};
- element.latestPatchsetNumber = 1 as PatchSetNumber;
+ element.latestPatchsetNumber = 1 as RevisionPatchSetNum;
await element.updateComplete;
storageService = testResolver(storageServiceToken);
});
@@ -446,7 +444,7 @@
test('showAlert', async () => {
const promise = mockPromise();
- element.addEventListener(EventType.SHOW_ALERT, e => {
+ element.addEventListener('show-alert', e => {
assert.deepEqual(e.detail, {message: 'test message', showDismiss: true});
assert.isTrue(e.bubbles);
promise.resolve();
@@ -534,7 +532,7 @@
};
const alertStub = sinon.stub();
- element.addEventListener(EventType.SHOW_ALERT, alertStub);
+ element.addEventListener('show-alert', alertStub);
return element.getFileData().then(async () => {
await element.updateComplete;
@@ -566,7 +564,7 @@
};
const alertStub = sinon.stub();
- element.addEventListener(EventType.SHOW_ALERT, alertStub);
+ element.addEventListener('show-alert', alertStub);
return element.getFileData().then(async () => {
await element.updateComplete;
diff --git a/polygerrit-ui/app/elements/gr-app-element.ts b/polygerrit-ui/app/elements/gr-app-element.ts
index e5991f6..98f1f25 100644
--- a/polygerrit-ui/app/elements/gr-app-element.ts
+++ b/polygerrit-ui/app/elements/gr-app-element.ts
@@ -47,7 +47,6 @@
import {GrSettingsView} from './settings/gr-settings-view/gr-settings-view';
import {
DialogChangeEventDetail,
- EventType,
PageErrorEventDetail,
RpcLogEvent,
TitleChangeEventDetail,
@@ -175,19 +174,19 @@
constructor() {
super();
- document.addEventListener(EventType.PAGE_ERROR, e => {
+ document.addEventListener('page-error', e => {
this.handlePageError(e);
});
- this.addEventListener(EventType.TITLE_CHANGE, e => {
+ this.addEventListener('title-change', e => {
this.handleTitleChange(e);
});
- this.addEventListener(EventType.DIALOG_CHANGE, e => {
+ this.addEventListener('dialog-change', e => {
this.handleDialogChange(e as CustomEvent<DialogChangeEventDetail>);
});
- document.addEventListener(EventType.LOCATION_CHANGE, () =>
+ document.addEventListener('location-change', () =>
this.handleLocationChange()
);
- document.addEventListener(EventType.GR_RPC_LOG, e => this.handleRpcLog(e));
+ document.addEventListener('gr-rpc-log', e => this.handleRpcLog(e));
this.shortcuts.addAbstract(Shortcut.OPEN_SHORTCUT_HELP_DIALOG, () =>
this.showKeyboardShortcuts()
);
diff --git a/polygerrit-ui/app/elements/plugins/gr-attribute-helper/gr-attribute-helper.ts b/polygerrit-ui/app/elements/plugins/gr-attribute-helper/gr-attribute-helper.ts
index bc4e701..b34e1c3 100644
--- a/polygerrit-ui/app/elements/plugins/gr-attribute-helper/gr-attribute-helper.ts
+++ b/polygerrit-ui/app/elements/plugins/gr-attribute-helper/gr-attribute-helper.ts
@@ -6,6 +6,7 @@
import {AttributeHelperPluginApi} from '../../../api/attribute-helper';
import {PluginApi} from '../../../api/plugin';
import {ReportingService} from '../../../services/gr-reporting/gr-reporting';
+import {ValueChangedEvent} from '../../../types/events';
export class GrAttributeHelper implements AttributeHelperPluginApi {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
@@ -51,7 +52,7 @@
bind(name: string, callback: (value: any) => void) {
this.reporting.trackApi(this.plugin, 'attribute', 'bind');
const attributeChangedEventName = this._getChangedEventName(name);
- const changedHandler = (e: CustomEvent) =>
+ const changedHandler = (e: ValueChangedEvent) =>
this._reportValue(callback, e.detail.value);
const unbind = () =>
this.element.removeEventListener(
diff --git a/polygerrit-ui/app/elements/plugins/gr-endpoint-param/gr-endpoint-param.ts b/polygerrit-ui/app/elements/plugins/gr-endpoint-param/gr-endpoint-param.ts
index e73aad6..d3429fe 100644
--- a/polygerrit-ui/app/elements/plugins/gr-endpoint-param/gr-endpoint-param.ts
+++ b/polygerrit-ui/app/elements/plugins/gr-endpoint-param/gr-endpoint-param.ts
@@ -5,6 +5,7 @@
*/
import {LitElement, PropertyValues} from 'lit';
import {customElement, property} from 'lit/decorators.js';
+import {fireNoBubbleNoCompose} from '../../../utils/event-util';
declare global {
interface HTMLElementTagNameMap {
@@ -22,9 +23,7 @@
override willUpdate(changedProperties: PropertyValues) {
if (changedProperties.has('value')) {
- this.dispatchEvent(
- new CustomEvent('value-changed', {detail: {value: this.value}})
- );
+ fireNoBubbleNoCompose(this, 'value-changed', {value: this.value});
}
}
}
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 0e75abb..89513e3 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
@@ -15,7 +15,7 @@
import {AccountDetailInfo, ServerInfo} from '../../../types/common';
import {EditableAccountField} from '../../../constants/constants';
import {getAppContext} from '../../../services/app-context';
-import {fire, fireEvent} from '../../../utils/event-util';
+import {fire} from '../../../utils/event-util';
import {LitElement, css, html, nothing, PropertyValues} from 'lit';
import {customElement, property, state} from 'lit/decorators.js';
import {sharedStyles} from '../../../styles/shared-styles';
@@ -25,12 +25,6 @@
@customElement('gr-account-info')
export class GrAccountInfo extends LitElement {
- /**
- * Fired when account details are changed.
- *
- * @event account-detail-update
- */
-
// private but used in test
@state() nameMutable?: boolean;
@@ -341,7 +335,7 @@
this.hasDisplayNameChange = false;
this.hasStatusChange = false;
this.saving = false;
- fireEvent(this, 'account-detail-update');
+ fire(this, 'account-detail-update', {});
});
}
@@ -410,6 +404,8 @@
declare global {
interface HTMLElementEventMap {
'unsaved-changes-changed': ValueChangedEvent<boolean>;
+ /** Fired when account details are changed. */
+ 'account-detail-update': CustomEvent<{}>;
}
interface HTMLElementTagNameMap {
'gr-account-info': GrAccountInfo;
diff --git a/polygerrit-ui/app/elements/settings/gr-registration-dialog/gr-registration-dialog.ts b/polygerrit-ui/app/elements/settings/gr-registration-dialog/gr-registration-dialog.ts
index a20c0ee..c6c023e 100644
--- a/polygerrit-ui/app/elements/settings/gr-registration-dialog/gr-registration-dialog.ts
+++ b/polygerrit-ui/app/elements/settings/gr-registration-dialog/gr-registration-dialog.ts
@@ -9,7 +9,7 @@
import {ServerInfo, AccountDetailInfo} from '../../../types/common';
import {EditableAccountField} from '../../../constants/constants';
import {getAppContext} from '../../../services/app-context';
-import {fireEvent} from '../../../utils/event-util';
+import {fire} from '../../../utils/event-util';
import {LitElement, css, html, PropertyValues} from 'lit';
import {customElement, property, query, state} from 'lit/decorators.js';
import {sharedStyles} from '../../../styles/shared-styles';
@@ -27,12 +27,6 @@
@customElement('gr-registration-dialog')
export class GrRegistrationDialog extends LitElement {
/**
- * Fired when account details are changed.
- *
- * @event account-detail-update
- */
-
- /**
* Fired when the close button is pressed.
*
* @event close
@@ -293,7 +287,7 @@
return Promise.all(promises).then(() => {
this.saving = false;
- fireEvent(this, 'account-detail-update');
+ fire(this, 'account-detail-update', {});
});
}
@@ -309,7 +303,7 @@
private close() {
this.saving = true; // disable buttons indefinitely
- fireEvent(this, 'close');
+ fire(this, 'close', {});
}
// private but used in test
diff --git a/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view_test.ts b/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view_test.ts
index a5ef86d..a0d7fab 100644
--- a/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view_test.ts
+++ b/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view_test.ts
@@ -38,7 +38,6 @@
} from '../../../test/test-data-generators';
import {GrSelect} from '../../shared/gr-select/gr-select';
import {fixture, html, assert} from '@open-wc/testing';
-import {EventType} from '../../../types/events';
suite('gr-settings-view tests', () => {
let element: GrSettingsView;
@@ -915,7 +914,7 @@
await element._testOnly_loadingPromise;
assert.equal(
(dispatchEventSpy.lastCall.args[0] as CustomEvent).type,
- EventType.SHOW_ALERT
+ 'show-alert'
);
assert.deepEqual(
(dispatchEventSpy.lastCall.args[0] as CustomEvent).detail,
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 31e4f5d..d7c6c8b 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
@@ -17,6 +17,8 @@
import {customElement, property} from 'lit/decorators.js';
import {ClassInfo, classMap} from 'lit/directives/class-map.js';
import {getLabelStatus, hasVoted, LabelStatus} from '../../../utils/label-util';
+import {fire} from '../../../utils/event-util';
+import {RemoveAccountEvent} from '../../../types/events';
@customElement('gr-account-chip')
export class GrAccountChip extends LitElement {
@@ -196,13 +198,8 @@
private handleRemoveTap(e: MouseEvent) {
e.preventDefault();
- this.dispatchEvent(
- new CustomEvent('remove', {
- detail: {account: this.account},
- composed: true,
- bubbles: true,
- })
- );
+ if (!this.account) return;
+ fire(this, 'remove-account', {account: this.account});
}
private getHasAvatars() {
@@ -232,4 +229,7 @@
interface HTMLElementTagNameMap {
'gr-account-chip': GrAccountChip;
}
+ interface HTMLElementEventMap {
+ 'remove-account': RemoveAccountEvent;
+ }
}
diff --git a/polygerrit-ui/app/elements/shared/gr-account-entry/gr-account-entry.ts b/polygerrit-ui/app/elements/shared/gr-account-entry/gr-account-entry.ts
index 0509925..496dff1 100644
--- a/polygerrit-ui/app/elements/shared/gr-account-entry/gr-account-entry.ts
+++ b/polygerrit-ui/app/elements/shared/gr-account-entry/gr-account-entry.ts
@@ -5,15 +5,17 @@
*/
import '../gr-autocomplete/gr-autocomplete';
import {
+ AutocompleteCommitEvent,
AutocompleteQuery,
GrAutocomplete,
} from '../gr-autocomplete/gr-autocomplete';
import {sharedStyles} from '../../../styles/shared-styles';
import {LitElement, PropertyValues, html, css} from 'lit';
import {customElement, property, query, state} from 'lit/decorators.js';
-import {BindValueChangeEvent} from '../../../types/events';
+import {AddAccountEvent, BindValueChangeEvent} from '../../../types/events';
import {SuggestedReviewerInfo} from '../../../types/common';
import {PaperInputElement} from '@polymer/paper-input/paper-input';
+import {fire} from '../../../utils/event-util';
/**
* gr-account-entry is an element for entering account
@@ -23,20 +25,6 @@
export class GrAccountEntry extends LitElement {
@query('#input') private input?: GrAutocomplete;
- /**
- * Fired when an account is entered.
- *
- * @event add
- */
-
- /**
- * When allowAnyInput is true, account-text-changed is fired when input text
- * changed. This is needed so that the reply dialog's save button can be
- * enabled for arbitrary cc's, which don't need a 'commit'.
- *
- * @event account-text-changed
- */
-
@property({type: Boolean})
allowAnyInput = false;
@@ -110,22 +98,14 @@
return this.input!.text;
}
- private handleInputCommit(e: CustomEvent) {
- this.dispatchEvent(
- new CustomEvent('add', {
- detail: {value: e.detail.value},
- composed: true,
- bubbles: true,
- })
- );
+ private handleInputCommit(e: AutocompleteCommitEvent) {
+ fire(this, 'add', {value: e.detail.value});
this.input!.focus();
}
private inputTextChanged() {
if (this.inputText.length && this.allowAnyInput) {
- this.dispatchEvent(
- new CustomEvent('account-text-changed', {bubbles: true, composed: true})
- );
+ fire(this, 'account-text-changed', {});
}
}
@@ -138,4 +118,15 @@
interface HTMLElementTagNameMap {
'gr-account-entry': GrAccountEntry;
}
+ interface HTMLElementEventMap {
+ /** Fired when an account is entered. */
+ // prettier-ignore
+ 'add': AddAccountEvent;
+ /**
+ * When allowAnyInput is true, account-text-changed is fired when input text
+ * changed. This is needed so that the reply dialog's save button can be
+ * enabled for arbitrary cc's, which don't need a 'commit'.
+ */
+ 'account-text-changed': CustomEvent<{}>;
+ }
}
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 bb0200a..cf7ff2209 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
@@ -13,9 +13,8 @@
import {isSelf, isServiceUser} from '../../../utils/account-util';
import {ChangeInfo, AccountInfo, ServerInfo} from '../../../types/common';
import {assertIsDefined, hasOwnProperty} from '../../../utils/common-util';
-import {fireEvent} from '../../../utils/event-util';
+import {fire} from '../../../utils/event-util';
import {isInvolved} from '../../../utils/change-util';
-import {EventType, ShowAlertEventDetail} from '../../../types/events';
import {LitElement, css, html, TemplateResult} from 'lit';
import {customElement, property, state} from 'lit/decorators.js';
import {classMap} from 'lit/directives/class-map.js';
@@ -364,16 +363,10 @@
e.stopPropagation();
if (!this.account._account_id) return;
- this.dispatchEvent(
- new CustomEvent<ShowAlertEventDetail>(EventType.SHOW_ALERT, {
- detail: {
- message: 'Saving attention set update ...',
- dismissOnNavigation: true,
- },
- composed: true,
- bubbles: true,
- })
- );
+ fire(this, 'show-alert', {
+ message: 'Saving attention set update ...',
+ dismissOnNavigation: true,
+ });
// We are deliberately updating the UI before making the API call. It is a
// risk that we are taking to achieve a better UX for 99.9% of the cases.
@@ -394,7 +387,7 @@
reason
)
.then(() => {
- fireEvent(this, 'hide-alert');
+ fire(this, 'hide-alert', {});
});
}
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 65d8859..8339df9 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
@@ -38,18 +38,17 @@
import {ReviewerState} from '../../../api/rest-api';
const VALID_EMAIL_ALERT = 'Please input a valid email.';
+const VALID_USER_GROUP_ALERT = 'Please input a valid user or group.';
declare global {
interface HTMLElementEventMap {
'accounts-changed': ValueChangedEvent<(AccountInfo | GroupInfo)[]>;
'pending-confirmation-changed': ValueChangedEvent<SuggestedReviewerGroupInfo | null>;
+ 'account-added': CustomEvent<{account: AccountInfo | GroupInfo}>;
}
interface HTMLElementTagNameMap {
'gr-account-list': GrAccountList;
}
- interface HTMLElementEventMap {
- 'account-added': CustomEvent<AccountInputDetail>;
- }
}
export interface AccountInputDetail {
account: AccountInput;
@@ -153,7 +152,7 @@
constructor() {
super();
this.querySuggestions = input => this.getSuggestions(input);
- this.addEventListener('remove', e =>
+ this.addEventListener('remove-account', e =>
this.handleRemove(e as CustomEvent<{account: AccountInput}>)
);
}
@@ -264,24 +263,28 @@
this.addAccountItem(item);
}
- addAccountItem(item: RawAccountInput) {
+ /**
+ * Check if account or group is valid and add it.
+ *
+ * @return true if account or group is added.
+ */
+ addAccountItem(item: RawAccountInput): boolean {
// Append new account or group to the accounts property. We add our own
// internal properties to the account/group here, so we clone the object
// to avoid cluttering up the shared change object.
- let account;
- let group;
+ let accountOrGroup: AccountInfo | GroupInfo | undefined;
let itemTypeAdded = 'unknown';
if (isAccountObject(item)) {
- account = {...item.account};
- this.accounts.push(account);
+ accountOrGroup = {...item.account};
+ this.accounts.push(accountOrGroup);
itemTypeAdded = 'account';
} else if (isSuggestedReviewerGroupInfo(item)) {
if (item.confirm) {
this.pendingConfirmation = item;
- return;
+ return false;
}
- group = {...item.group};
- this.accounts.push(group);
+ accountOrGroup = {...item.group};
+ this.accounts.push(accountOrGroup);
itemTypeAdded = 'group';
} else if (this.allowAnyInput) {
if (!item.includes('@')) {
@@ -291,13 +294,17 @@
fireAlert(this, VALID_EMAIL_ALERT);
return false;
} else {
- account = {email: item as EmailAddress};
- this.accounts.push(account);
+ accountOrGroup = {email: item as EmailAddress};
+ this.accounts.push(accountOrGroup);
itemTypeAdded = 'email';
}
}
+ if (!accountOrGroup) {
+ fireAlert(this, VALID_USER_GROUP_ALERT);
+ return false;
+ }
fire(this, 'accounts-changed', {value: this.accounts.slice()});
- fire(this, 'account-added', {account: (account ?? group)! as AccountInput});
+ fire(this, 'account-added', {account: accountOrGroup});
this.reporting.reportInteraction(`Add to ${this.id}`, {itemTypeAdded});
this.pendingConfirmation = null;
this.requestUpdate();
diff --git a/polygerrit-ui/app/elements/shared/gr-account-list/gr-account-list_test.ts b/polygerrit-ui/app/elements/shared/gr-account-list/gr-account-list_test.ts
index cc2723e..e7112df 100644
--- a/polygerrit-ui/app/elements/shared/gr-account-list/gr-account-list_test.ts
+++ b/polygerrit-ui/app/elements/shared/gr-account-list/gr-account-list_test.ts
@@ -35,7 +35,6 @@
import {createChange} from '../../../test/test-data-generators';
import {ReviewerState} from '../../../api/rest-api';
import {fixture, html, assert} from '@open-wc/testing';
-import {EventType} from '../../../types/events';
class MockSuggestionsProvider implements ReviewerSuggestionsProvider {
init() {}
@@ -154,7 +153,7 @@
// Removed accounts are taken out of the list.
element.dispatchEvent(
- new CustomEvent('remove', {
+ new CustomEvent('remove-account', {
detail: {account: existingAccount1},
composed: true,
bubbles: true,
@@ -168,14 +167,14 @@
// Invalid remove is ignored.
element.dispatchEvent(
- new CustomEvent('remove', {
+ new CustomEvent('remove-account', {
detail: {account: existingAccount1},
composed: true,
bubbles: true,
})
);
element.dispatchEvent(
- new CustomEvent('remove', {
+ new CustomEvent('remove-account', {
detail: {account: newAccount},
composed: true,
bubbles: true,
@@ -197,7 +196,7 @@
// Removed groups are taken out of the list.
element.dispatchEvent(
- new CustomEvent('remove', {
+ new CustomEvent('remove-account', {
detail: {account: newGroup},
composed: true,
bubbles: true,
@@ -289,6 +288,15 @@
assert.isFalse(element.computeRemovable(newAccount));
});
+ test('addAccountItem with invalid item', () => {
+ const toastHandler = sinon.stub();
+ element.allowAnyInput = false;
+ element.addEventListener('show-alert', toastHandler);
+ const result = element.addAccountItem('test');
+ assert.isFalse(result);
+ assert.isTrue(toastHandler.called);
+ });
+
test('submitEntryText', async () => {
element.allowAnyInput = true;
await element.updateComplete;
@@ -425,7 +433,7 @@
test('toasts on invalid email', () => {
const toastHandler = sinon.stub();
- element.addEventListener(EventType.SHOW_ALERT, toastHandler);
+ element.addEventListener('show-alert', toastHandler);
handleAdd('test');
assert.isTrue(toastHandler.called);
});
diff --git a/polygerrit-ui/app/elements/shared/gr-autocomplete-dropdown/gr-autocomplete-dropdown.ts b/polygerrit-ui/app/elements/shared/gr-autocomplete-dropdown/gr-autocomplete-dropdown.ts
index 91e601c..4b27948 100644
--- a/polygerrit-ui/app/elements/shared/gr-autocomplete-dropdown/gr-autocomplete-dropdown.ts
+++ b/polygerrit-ui/app/elements/shared/gr-autocomplete-dropdown/gr-autocomplete-dropdown.ts
@@ -6,7 +6,7 @@
import '../gr-cursor-manager/gr-cursor-manager';
import '../../../styles/shared-styles';
import {GrCursorManager} from '../gr-cursor-manager/gr-cursor-manager';
-import {fireEvent} from '../../../utils/event-util';
+import {fire} from '../../../utils/event-util';
import {Key} from '../../../utils/dom-util';
import {FitController} from '../../lit/fit-controller';
import {css, html, LitElement, PropertyValues} from 'lit';
@@ -20,6 +20,9 @@
interface HTMLElementTagNameMap {
'gr-autocomplete-dropdown': GrAutocompleteDropdown;
}
+ interface HTMLElementEventMap {
+ 'dropdown-closed': CustomEvent<{}>;
+ }
}
export interface Item {
@@ -30,11 +33,21 @@
value?: string;
}
-export interface ItemSelectedEvent {
+export interface ItemSelectedEventDetail {
trigger: string;
selected: HTMLElement | null;
}
+export enum AutocompleteQueryStatusType {
+ LOADING = 'loading',
+ ERROR = 'error',
+}
+
+export interface AutocompleteQueryStatus {
+ type: AutocompleteQueryStatusType;
+ message: string;
+}
+
@customElement('gr-autocomplete-dropdown')
export class GrAutocompleteDropdown extends LitElement {
/**
@@ -58,8 +71,8 @@
/** If specified a single non-interactable line is shown instead of
* suggestions.
*/
- @property({type: String})
- errorMessage?: String;
+ @property({type: Object})
+ queryStatus?: AutocompleteQueryStatus;
@property({type: Number})
verticalOffset = 0;
@@ -117,10 +130,12 @@
li.selected {
background-color: var(--hover-background-color);
}
- li.query-error {
+ li.query-status {
background-color: var(--disabled-background);
- color: var(--error-foreground);
cursor: default;
+ }
+ li.query-status.error {
+ color: var(--error-foreground);
white-space: pre-wrap;
}
@media only screen and (max-height: 35em) {
@@ -140,7 +155,7 @@
}
private isSuggestionListInteractible() {
- return !this.isHidden && !this.errorMessage;
+ return !this.isHidden && !this.queryStatus;
}
constructor() {
@@ -172,7 +187,8 @@
override updated(changedProperties: PropertyValues) {
if (
changedProperties.has('suggestions') ||
- changedProperties.has('isHidden')
+ changedProperties.has('isHidden') ||
+ changedProperties.has('queryStatus')
) {
if (!this.isHidden) {
this.computeCursorStopsAndRefit();
@@ -180,15 +196,19 @@
}
}
- private renderError() {
+ private renderStatus() {
return html`
<li
tabindex="-1"
- aria-label="autocomplete query error"
- class="query-error"
+ aria-label="autocomplete query status"
+ class="query-status ${this.queryStatus?.type}"
>
- <span>${this.errorMessage}</span>
- <span class="label">ERROR</span>
+ <span>${this.queryStatus?.message}</span>
+ <span class="label"
+ >${this.queryStatus?.type === AutocompleteQueryStatusType.ERROR
+ ? 'ERROR'
+ : ''}</span
+ >
</li>
`;
}
@@ -198,8 +218,8 @@
<div class="dropdown-content" id="suggestions" role="listbox">
<ul>
${when(
- this.errorMessage,
- () => this.renderError(),
+ this.queryStatus,
+ () => this.renderStatus(),
() => html`
${repeat(
this.suggestions,
@@ -236,7 +256,7 @@
}
getCurrentText() {
- if (!this.errorMessage) {
+ if (!this.queryStatus) {
return this.getCursorTarget()?.dataset['value'] || '';
}
return '';
@@ -257,32 +277,20 @@
// private but used in tests
handleTab() {
if (this.isSuggestionListInteractible()) {
- this.dispatchEvent(
- new CustomEvent<ItemSelectedEvent>('item-selected', {
- detail: {
- trigger: 'tab',
- selected: this.cursor.target,
- },
- composed: true,
- bubbles: true,
- })
- );
+ fire(this, 'item-selected', {
+ trigger: 'tab',
+ selected: this.cursor.target,
+ });
}
}
// private but used in tests
handleEnter() {
if (this.isSuggestionListInteractible()) {
- this.dispatchEvent(
- new CustomEvent<ItemSelectedEvent>('item-selected', {
- detail: {
- trigger: 'enter',
- selected: this.cursor.target,
- },
- composed: true,
- bubbles: true,
- })
- );
+ fire(this, 'item-selected', {
+ trigger: 'enter',
+ selected: this.cursor.target,
+ });
}
}
@@ -301,20 +309,14 @@
}
selected = selected.parentElement!;
}
- this.dispatchEvent(
- new CustomEvent<ItemSelectedEvent>('item-selected', {
- detail: {
- trigger: 'click',
- selected,
- },
- composed: true,
- bubbles: true,
- })
- );
+ fire(this, 'item-selected', {
+ trigger: 'click',
+ selected,
+ });
}
private fireClose() {
- fireEvent(this, 'dropdown-closed');
+ fire(this, 'dropdown-closed', {});
}
getCursorTarget() {
diff --git a/polygerrit-ui/app/elements/shared/gr-autocomplete-dropdown/gr-autocomplete-dropdown_test.ts b/polygerrit-ui/app/elements/shared/gr-autocomplete-dropdown/gr-autocomplete-dropdown_test.ts
index 54d054b..10ba5d0 100644
--- a/polygerrit-ui/app/elements/shared/gr-autocomplete-dropdown/gr-autocomplete-dropdown_test.ts
+++ b/polygerrit-ui/app/elements/shared/gr-autocomplete-dropdown/gr-autocomplete-dropdown_test.ts
@@ -5,7 +5,10 @@
*/
import '../../../test/common-test-setup';
import './gr-autocomplete-dropdown';
-import {GrAutocompleteDropdown} from './gr-autocomplete-dropdown';
+import {
+ AutocompleteQueryStatusType,
+ GrAutocompleteDropdown,
+} from './gr-autocomplete-dropdown';
import {
pressKey,
queryAll,
@@ -177,7 +180,7 @@
});
});
- suite('error tests', () => {
+ suite('status tests', () => {
let element: GrAutocompleteDropdown;
setup(async () => {
@@ -185,7 +188,10 @@
html`<gr-autocomplete-dropdown></gr-autocomplete-dropdown>`
);
element.open();
- element.errorMessage = 'Failed query error';
+ element.queryStatus = {
+ type: AutocompleteQueryStatusType.ERROR,
+ message: 'Failed query error',
+ };
await waitEventLoop();
});
@@ -200,8 +206,8 @@
<div class="dropdown-content" id="suggestions" role="listbox">
<ul>
<li
- aria-label="autocomplete query error"
- class="query-error"
+ aria-label="autocomplete query status"
+ class="query-status error"
tabindex="-1"
>
<span>Failed query error</span>
@@ -213,6 +219,31 @@
);
});
+ test('renders loading', async () => {
+ element.queryStatus = {
+ type: AutocompleteQueryStatusType.LOADING,
+ message: 'Loading...',
+ };
+ await waitEventLoop();
+ assert.shadowDom.equal(
+ element,
+ /* HTML */ `
+ <div class="dropdown-content" id="suggestions" role="listbox">
+ <ul>
+ <li
+ aria-label="autocomplete query status"
+ class="query-status loading"
+ tabindex="-1"
+ >
+ <span>Loading...</span>
+ <span class="label"></span>
+ </li>
+ </ul>
+ </div>
+ `
+ );
+ });
+
test('escape key close dropdown with error', async () => {
const closeSpy = sinon.spy(element, 'close');
pressKey(element, Key.ESC);
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 f8f7f9d..a2be983 100644
--- a/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete.ts
+++ b/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete.ts
@@ -7,8 +7,13 @@
import '../gr-autocomplete-dropdown/gr-autocomplete-dropdown';
import '../gr-cursor-manager/gr-cursor-manager';
import '../../../styles/shared-styles';
-import {GrAutocompleteDropdown} from '../gr-autocomplete-dropdown/gr-autocomplete-dropdown';
-import {fire, fireEvent} from '../../../utils/event-util';
+import {
+ AutocompleteQueryStatus,
+ AutocompleteQueryStatusType,
+ GrAutocompleteDropdown,
+ ItemSelectedEventDetail,
+} from '../gr-autocomplete-dropdown/gr-autocomplete-dropdown';
+import {fire} from '../../../utils/event-util';
import {
debounce,
DelayedTask,
@@ -69,13 +74,6 @@
*/
/**
- * Fired on keydown to allow for custom hooks into autocomplete textbox
- * behavior.
- *
- * @event input-keydown
- */
-
- /**
* Query for requesting autocomplete suggestions. The function should
* accept the input as a string parameter and return a promise. The
* promise yields an array of suggestion objects with "name", "label",
@@ -166,7 +164,7 @@
@state() suggestions: AutocompleteSuggestion[] = [];
- @state() queryErrorMessage?: string;
+ @state() queryStatus?: AutocompleteQueryStatus;
@state() index: number | null = null;
@@ -179,8 +177,23 @@
@state() selected: HTMLElement | null = null;
+ /**
+ * The query id that status or suggestions correspond to.
+ */
+ private activeQueryId = 0;
+
+ /**
+ * Last scheduled update suggestions task.
+ */
private updateSuggestionsTask?: DelayedTask;
+ // Generate ids for scheduled suggestion queries to easily distinguish them.
+ private static NEXT_QUERY_ID = 1;
+
+ private static getNextQueryId() {
+ return GrAutocomplete.NEXT_QUERY_ID++;
+ }
+
/**
* @return Promise that resolves when suggestions are update.
*/
@@ -266,7 +279,7 @@
}
if (
changedProperties.has('suggestions') ||
- changedProperties.has('queryErrorMessage')
+ changedProperties.has('queryStatus')
) {
this.updateDropdownVisibility();
}
@@ -286,7 +299,7 @@
class=${this.computeClass()}
?disabled=${this.disabled}
.value=${this.text}
- @value-changed=${(e: CustomEvent) => {
+ @value-changed=${(e: ValueChangedEvent) => {
this.text = e.detail.value;
}}
.placeholder=${this.placeholder}
@@ -310,7 +323,7 @@
@item-selected=${this.handleItemSelect}
@dropdown-closed=${this.focusWithoutDisplayingSuggestions}
.suggestions=${this.suggestions}
- .errorMessage=${this.queryErrorMessage}
+ .queryStatus=${this.queryStatus}
role="listbox"
.index=${this.index}
>
@@ -347,14 +360,16 @@
this.text = '';
}
- private handleItemSelectEnter(e: CustomEvent | KeyboardEvent) {
+ private handleItemSelectEnter(
+ e: CustomEvent<ItemSelectedEventDetail> | KeyboardEvent
+ ) {
this.handleInputCommit();
e.stopPropagation();
e.preventDefault();
this.focusWithoutDisplayingSuggestions();
}
- handleItemSelect(e: CustomEvent) {
+ handleItemSelect(e: CustomEvent<ItemSelectedEventDetail>) {
if (e.detail.trigger === 'click') {
this.selected = e.detail.selected;
this._commit();
@@ -412,8 +427,7 @@
// Reset suggestions for every update
// This will also prevent from carrying over suggestions:
// @see Issue 12039
- this.suggestions = [];
- this.queryErrorMessage = undefined;
+ this.resetQueryOutput();
// TODO(taoalpha): Also skip if text has not changed
@@ -421,8 +435,7 @@
return;
}
- const query = this.query;
- if (!query) {
+ if (!this.query) {
return;
}
@@ -435,50 +448,69 @@
return;
}
- const requestText = this.text;
- const update = () => {
- query(this.text)
- .then(suggestions => {
- if (requestText !== this.text) {
- // Late response.
- return;
- }
- for (const suggestion of suggestions) {
- suggestion.text = suggestion?.name ?? '';
- }
- this.suggestions = suggestions;
- if (this.index === -1) {
- this.value = '';
- }
- })
- .catch(e => {
- this.value = '';
- if (typeof e === 'string') {
- this.queryErrorMessage = e;
- } else if (e instanceof Error) {
- this.queryErrorMessage = e.message;
- }
- });
- };
-
+ const queryId = GrAutocomplete.getNextQueryId();
+ this.activeQueryId = queryId;
+ this.setQueryStatus({
+ type: AutocompleteQueryStatusType.LOADING,
+ message: 'Loading...',
+ });
this.updateSuggestionsTask = debounce(
this.updateSuggestionsTask,
- update,
+ this.createUpdateTask(queryId, this.query, this.text),
DEBOUNCE_WAIT_MS
);
}
+ private createUpdateTask(
+ queryId: number,
+ query: AutocompleteQuery,
+ text: string
+ ): () => Promise<void> {
+ return async () => {
+ let suggestions: AutocompleteSuggestion[];
+ try {
+ suggestions = await query(text);
+ } catch (e) {
+ this.value = '';
+ if (typeof e === 'string') {
+ this.setQueryStatus({
+ type: AutocompleteQueryStatusType.ERROR,
+ message: e,
+ });
+ } else if (e instanceof Error) {
+ this.setQueryStatus({
+ type: AutocompleteQueryStatusType.ERROR,
+ message: e.message,
+ });
+ }
+ return;
+ }
+ if (queryId !== this.activeQueryId) {
+ // Late response.
+ return;
+ }
+ for (const suggestion of suggestions) {
+ suggestion.text = suggestion?.name ?? '';
+ }
+ this.setSuggestions(suggestions);
+ if (this.index === -1) {
+ this.value = '';
+ }
+ };
+ }
+
setFocus(focused: boolean) {
if (focused === this.focused) return;
this.focused = focused;
this.updateDropdownVisibility();
}
+ private shouldShowDropdown() {
+ return (this.suggestions.length > 0 || this.queryStatus) && this.focused;
+ }
+
updateDropdownVisibility() {
- if (
- (this.suggestions.length > 0 || this.queryErrorMessage) &&
- this.focused
- ) {
+ if (this.shouldShowDropdown()) {
this.suggestionsDropdown?.open();
return;
}
@@ -511,10 +543,26 @@
this.cancel();
break;
case 'Tab':
- if (this.suggestions.length > 0 && this.tabComplete) {
+ if (
+ this.queryStatus?.type === AutocompleteQueryStatusType.LOADING &&
+ this.tabComplete
+ ) {
e.preventDefault();
+ // Queue tab on load.
+ this.queryStatus = {
+ type: AutocompleteQueryStatusType.LOADING,
+ message: 'Loading... (Handle Tab on load)',
+ };
+ const queryId = this.activeQueryId;
+ this.latestSuggestionUpdateComplete?.then(() => {
+ if (queryId === this.activeQueryId) {
+ this.handleInputCommit(/* _tabComplete=*/ true);
+ }
+ });
+ } else if (this.suggestions.length > 0 && this.tabComplete) {
+ e.preventDefault();
+ this.handleInputCommit(/* _tabComplete=*/ true);
this.focus();
- this.handleInputCommit(true);
} else {
this.setFocus(false);
}
@@ -523,12 +571,24 @@
if (modifierPressed(e)) {
break;
}
- if (this.suggestions.length > 0) {
+ e.preventDefault();
+ if (this.queryStatus?.type === AutocompleteQueryStatusType.LOADING) {
+ // Queue enter on load.
+ this.queryStatus = {
+ type: AutocompleteQueryStatusType.LOADING,
+ message: 'Loading... (Handle Enter on load)',
+ };
+ const queryId = this.activeQueryId;
+ this.latestSuggestionUpdateComplete?.then(() => {
+ if (queryId === this.activeQueryId) {
+ this.handleItemSelectEnter(e);
+ }
+ });
+ } else if (this.suggestions.length > 0) {
// If suggestions are shown, act as if the keypress is in dropdown.
// suggestions length is 0 if error is shown.
this.handleItemSelectEnter(e);
} else {
- e.preventDefault();
this.handleInputCommit();
}
break;
@@ -541,24 +601,20 @@
// been based on a previous input. Clear them. This prevents an
// outdated suggestion from being used if the input keystroke is
// immediately followed by a commit keystroke. @see Issue 8655
- this.suggestions = [];
+ this.resetQueryOutput();
+ this.activeQueryId = 0;
}
- this.dispatchEvent(
- new CustomEvent('input-keydown', {
- detail: {key: e.key, input: this.input},
- composed: true,
- bubbles: true,
- })
- );
}
cancel() {
- if (this.suggestions.length || this.queryErrorMessage) {
- this.suggestions = [];
- this.queryErrorMessage = undefined;
+ if (this.shouldShowDropdown()) {
+ this.resetQueryOutput();
+ // If query is in flight by setting id to 0 we indicate that the results
+ // are outdated.
+ this.activeQueryId = 0;
this.requestUpdate();
} else {
- fireEvent(this, 'cancel');
+ fire(this, 'cancel', {});
}
}
@@ -566,7 +622,7 @@
// Nothing to do if no suggestions.
if (
!this.allowNonSuggestedValues &&
- (this.suggestionsDropdown?.isHidden || this.queryErrorMessage)
+ (this.suggestionsDropdown?.isHidden || this.suggestions.length === 0)
) {
return;
}
@@ -608,6 +664,7 @@
}
}
this.setFocus(false);
+ this.activeQueryId = 0;
};
/**
@@ -644,21 +701,31 @@
}
}
- this.suggestions = [];
- this.queryErrorMessage = undefined;
+ this.resetQueryOutput();
// we need willUpdate to send text-changed event before we can send the
// 'commit' event
await this.updateComplete;
if (!silent) {
- this.dispatchEvent(
- new CustomEvent('commit', {
- detail: {value} as AutocompleteCommitEventDetail,
- composed: true,
- bubbles: true,
- })
- );
+ fire(this, 'commit', {value});
}
}
+
+ // resetQueryOutput, setSuggestions and setQueryStatus insure that suggestions
+ // and queryStatus are never set at the same time.
+ private resetQueryOutput() {
+ this.suggestions = [];
+ this.queryStatus = undefined;
+ }
+
+ private setSuggestions(suggestions: AutocompleteSuggestion[]) {
+ this.suggestions = suggestions;
+ this.queryStatus = undefined;
+ }
+
+ private setQueryStatus(queryStatus: AutocompleteQueryStatus) {
+ this.suggestions = [];
+ this.queryStatus = queryStatus;
+ }
}
/**
diff --git a/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete_test.ts b/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete_test.ts
index 81949c7..0cef331 100644
--- a/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete_test.ts
+++ b/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete_test.ts
@@ -8,11 +8,15 @@
import {AutocompleteSuggestion, GrAutocomplete} from './gr-autocomplete';
import {
assertFails,
+ mockPromise,
pressKey,
queryAndAssert,
waitUntil,
} from '../../../test/test-utils';
-import {GrAutocompleteDropdown} from '../gr-autocomplete-dropdown/gr-autocomplete-dropdown';
+import {
+ AutocompleteQueryStatusType,
+ GrAutocompleteDropdown,
+} from '../gr-autocomplete-dropdown/gr-autocomplete-dropdown';
import {PaperInputElement} from '@polymer/paper-input/paper-input';
import {fixture, html, assert} from '@open-wc/testing';
import {Key, Modifier} from '../../../utils/dom-util';
@@ -30,9 +34,7 @@
const inputEl = () => queryAndAssert<HTMLInputElement>(element, '#input');
setup(async () => {
- element = await fixture(
- html`<gr-autocomplete no-debounce></gr-autocomplete>`
- );
+ element = await fixture(html`<gr-autocomplete></gr-autocomplete>`);
});
test('renders', () => {
@@ -151,7 +153,10 @@
],
}
);
- assert.equal(element.suggestionsDropdown?.errorMessage, 'blah not allowed');
+ assert.equal(
+ element.suggestionsDropdown?.queryStatus?.message,
+ 'blah not allowed'
+ );
});
test('cursor starts on suggestions', async () => {
@@ -240,17 +245,21 @@
await element.updateComplete;
return assertFails(promise).then(async () => {
+ await element.latestSuggestionUpdateComplete;
await waitUntil(() => !suggestionsEl().isHidden);
const cancelHandler = sinon.spy();
element.addEventListener('cancel', cancelHandler);
- assert.equal(element.queryErrorMessage, 'Test error');
+ assert.deepEqual(element.queryStatus, {
+ type: AutocompleteQueryStatusType.ERROR,
+ message: 'Test error',
+ });
pressKey(inputEl(), Key.ESC);
await waitUntil(() => suggestionsEl().isHidden);
assert.isFalse(cancelHandler.called);
- assert.isUndefined(element.queryErrorMessage);
+ assert.isUndefined(element.queryStatus);
pressKey(inputEl(), Key.ESC);
await element.updateComplete;
@@ -260,16 +269,14 @@
});
test('emits commit and handles cursor movement', async () => {
- let promise: Promise<AutocompleteSuggestion[]> = Promise.resolve([]);
- const queryStub = sinon.spy(
- (input: string) =>
- (promise = Promise.resolve([
- {name: input + ' 0', value: '0'},
- {name: input + ' 1', value: '1'},
- {name: input + ' 2', value: '2'},
- {name: input + ' 3', value: '3'},
- {name: input + ' 4', value: '4'},
- ] as AutocompleteSuggestion[]))
+ const queryStub = sinon.spy((input: string) =>
+ Promise.resolve([
+ {name: input + ' 0', value: '0'},
+ {name: input + ' 1', value: '1'},
+ {name: input + ' 2', value: '2'},
+ {name: input + ' 3', value: '3'},
+ {name: input + ' 4', value: '4'},
+ ] as AutocompleteSuggestion[])
);
element.query = queryStub;
await element.updateComplete;
@@ -280,7 +287,7 @@
element.text = 'blah';
await element.updateComplete;
- return promise.then(async () => {
+ return element.latestSuggestionUpdateComplete!.then(async () => {
await waitUntil(() => !suggestionsEl().isHidden);
const commitHandler = sinon.spy();
@@ -456,24 +463,22 @@
});
test('suggestions should not carry over', async () => {
- let promise: Promise<AutocompleteSuggestion[]> = Promise.resolve([]);
const queryStub = sinon
.stub()
- .returns(
- (promise = Promise.resolve([
- {name: 'suggestion', value: '0'},
- ] as AutocompleteSuggestion[]))
- );
+ .resolves([{name: 'suggestion', value: '0'}] as AutocompleteSuggestion[]);
element.query = queryStub;
focusOnInput();
element.text = 'bla';
await element.updateComplete;
- return promise.then(async () => {
+ return element.latestSuggestionUpdateComplete!.then(async () => {
await waitUntil(() => element.suggestions.length > 0);
assert.equal(element.suggestions.length, 1);
+
+ queryStub.resolves([] as AutocompleteSuggestion[]);
element.text = '';
element.threshold = 0;
await element.updateComplete;
+ await element.latestSuggestionUpdateComplete;
assert.equal(element.suggestions.length, 0);
});
});
@@ -488,11 +493,15 @@
element.text = 'bla';
await element.updateComplete;
return assertFails(promise).then(async () => {
- await waitUntil(() => element.queryErrorMessage === 'Test error');
+ await element.latestSuggestionUpdateComplete;
+ await waitUntil(() => element.queryStatus?.message === 'Test error');
+
+ queryStub.resolves([] as AutocompleteSuggestion[]);
element.text = '';
element.threshold = 0;
await element.updateComplete;
- assert.isUndefined(element.queryErrorMessage);
+ await element.latestSuggestionUpdateComplete;
+ assert.isUndefined(element.queryStatus);
});
});
@@ -514,6 +523,7 @@
return promise.then(async () => {
const commitHandler = sinon.spy();
element.addEventListener('commit', commitHandler);
+ await element.latestSuggestionUpdateComplete;
await waitUntil(() => element.suggestionsDropdown?.isHidden === false);
pressKey(inputEl(), Key.ENTER);
@@ -524,15 +534,24 @@
});
test('tabComplete flag functions', async () => {
+ element.query = sinon
+ .stub()
+ .resolves([
+ {name: 'tunnel snakes rule!', value: 'snakes'},
+ ] as AutocompleteSuggestion[]);
+
// commitHandler checks for the commit event, whereas commitSpy checks for
// the _commit function of the element.
const commitHandler = sinon.spy();
element.addEventListener('commit', commitHandler);
const commitSpy = sinon.spy(element, '_commit');
element.setFocus(true);
-
- element.suggestions = [{text: 'tunnel snakes rule!', name: ''}];
element.tabComplete = false;
+ element.text = 'text1';
+ await element.updateComplete;
+
+ await element.latestSuggestionUpdateComplete;
+ await element.updateComplete;
pressKey(inputEl(), Key.TAB);
await element.updateComplete;
@@ -540,9 +559,12 @@
assert.isFalse(commitSpy.called);
assert.isFalse(element.focused);
- element.tabComplete = true;
- await element.updateComplete;
element.setFocus(true);
+ element.tabComplete = true;
+ element.text = 'text2';
+ await element.updateComplete;
+
+ await element.latestSuggestionUpdateComplete;
await element.updateComplete;
pressKey(inputEl(), Key.TAB);
@@ -597,7 +619,11 @@
' allowNonSuggestedValues',
() => {
const commitStub = sinon.stub(element, '_commit');
- element.queryErrorMessage = 'Error';
+ element.queryStatus = {
+ type: AutocompleteQueryStatusType.ERROR,
+ message: 'Error',
+ };
+ element.suggestions = [];
element.handleInputCommit();
assert.isFalse(commitStub.called);
}
@@ -620,7 +646,11 @@
() => {
const commitStub = sinon.stub(element, '_commit');
element.allowNonSuggestedValues = true;
- element.queryErrorMessage = 'Error';
+ element.queryStatus = {
+ type: AutocompleteQueryStatusType.ERROR,
+ message: 'Error',
+ };
+ element.suggestions = [];
element.handleInputCommit();
assert.isTrue(commitStub.called);
}
@@ -629,6 +659,7 @@
test('handleInputCommit with autocomplete open calls commit', () => {
const commitStub = sinon.stub(element, '_commit');
suggestionsEl().isHidden = false;
+ element.suggestions = [{name: 'first suggestion'}];
element.handleInputCommit();
assert.isTrue(commitStub.calledOnce);
});
@@ -671,6 +702,215 @@
assert.equal(element.text, 'file:x');
});
+ test('render loading replace with suggestions when done', async () => {
+ const queryPromise = mockPromise<AutocompleteSuggestion[]>();
+ element.query = (_: string) => queryPromise;
+
+ element.setFocus(true);
+ element.text = 'blah';
+ await element.updateComplete;
+ await waitUntil(() => !suggestionsEl().isHidden);
+ assert.deepEqual(element.queryStatus, {
+ type: AutocompleteQueryStatusType.LOADING,
+ message: 'Loading...',
+ });
+
+ queryPromise.resolve([{name: 'suggestion 1'}] as AutocompleteSuggestion[]);
+ await element.latestSuggestionUpdateComplete;
+ await element.updateComplete;
+
+ assert.equal(element.suggestions.length, 1);
+ assert.isUndefined(element.queryStatus);
+ });
+
+ test('render loading replace with error when done', async () => {
+ const queryPromise = mockPromise<AutocompleteSuggestion[]>();
+ element.query = (_: string) => queryPromise;
+
+ element.setFocus(true);
+ element.text = 'blah';
+ await element.updateComplete;
+ await waitUntil(() => !suggestionsEl().isHidden);
+ assert.deepEqual(element.queryStatus, {
+ type: AutocompleteQueryStatusType.LOADING,
+ message: 'Loading...',
+ });
+
+ queryPromise.reject(new Error('Test error'));
+ await assertFails(queryPromise);
+ await element.latestSuggestionUpdateComplete;
+ await element.updateComplete;
+
+ assert.equal(element.suggestions.length, 0);
+ assert.deepEqual(element.queryStatus, {
+ type: AutocompleteQueryStatusType.ERROR,
+ message: 'Test error',
+ });
+ });
+
+ test('render loading esc cancels', async () => {
+ const queryPromise = mockPromise<AutocompleteSuggestion[]>();
+ element.query = (_: string) => queryPromise;
+
+ element.setFocus(true);
+ element.text = 'blah';
+ await element.updateComplete;
+ await waitUntil(() => !suggestionsEl().isHidden);
+ assert.deepEqual(element.queryStatus, {
+ type: AutocompleteQueryStatusType.LOADING,
+ message: 'Loading...',
+ });
+
+ const cancelHandler = sinon.spy();
+ element.addEventListener('cancel', cancelHandler);
+ pressKey(inputEl(), Key.ESC);
+ await waitUntil(() => suggestionsEl().isHidden);
+
+ assert.isFalse(cancelHandler.called);
+ assert.isUndefined(element.queryStatus);
+
+ pressKey(inputEl(), Key.ESC);
+ await element.updateComplete;
+
+ assert.isTrue(cancelHandler.called);
+ });
+
+ test('while loading queue enter commits', async () => {
+ const commitHandler = sinon.stub();
+ element.addEventListener('commit', commitHandler);
+ let resolvePromise: (value: AutocompleteSuggestion[]) => void;
+ const blockingPromise = new Promise<AutocompleteSuggestion[]>(resolve => {
+ resolvePromise = resolve;
+ });
+ element.query = (_: string) => blockingPromise;
+
+ element.setFocus(true);
+ element.text = 'blah';
+ await element.updateComplete;
+ await waitUntil(() => !suggestionsEl().isHidden);
+ assert.deepEqual(element.queryStatus, {
+ type: AutocompleteQueryStatusType.LOADING,
+ message: 'Loading...',
+ });
+
+ pressKey(inputEl(), Key.ENTER);
+ await element.updateComplete;
+ assert.deepEqual(element.queryStatus, {
+ type: AutocompleteQueryStatusType.LOADING,
+ message: 'Loading... (Handle Enter on load)',
+ });
+
+ resolvePromise!([{name: 'suggestion 1'}] as AutocompleteSuggestion[]);
+ await element.latestSuggestionUpdateComplete;
+ await element.updateComplete;
+
+ assert.equal(element.suggestions.length, 0);
+ assert.isUndefined(element.queryStatus);
+ assert.isTrue(commitHandler.called);
+ });
+
+ test('while loading queue tab completes', async () => {
+ element.tabComplete = true;
+ const commitHandler = sinon.stub();
+ element.addEventListener('commit', commitHandler);
+ const queryPromise = mockPromise<AutocompleteSuggestion[]>();
+ element.query = (_: string) => queryPromise;
+
+ element.setFocus(true);
+ element.text = 'blah';
+ await element.updateComplete;
+ await waitUntil(() => !suggestionsEl().isHidden);
+ assert.deepEqual(element.queryStatus, {
+ type: AutocompleteQueryStatusType.LOADING,
+ message: 'Loading...',
+ });
+
+ pressKey(inputEl(), Key.TAB);
+ await element.updateComplete;
+ assert.deepEqual(element.queryStatus, {
+ type: AutocompleteQueryStatusType.LOADING,
+ message: 'Loading... (Handle Tab on load)',
+ });
+
+ queryPromise.resolve([{name: 'suggestion 1'}] as AutocompleteSuggestion[]);
+ await element.latestSuggestionUpdateComplete;
+ await element.updateComplete;
+
+ assert.equal(element.suggestions.length, 0);
+ assert.isUndefined(element.queryStatus);
+ assert.isFalse(commitHandler.called);
+ assert.equal(element.text, 'suggestion 1');
+ });
+
+ test('while loading and queued update text cancels', async () => {
+ const commitHandler = sinon.stub();
+ element.addEventListener('commit', commitHandler);
+ const queryPromise = mockPromise<AutocompleteSuggestion[]>();
+ element.query = (_: string) => queryPromise;
+
+ element.setFocus(true);
+ element.text = 'blah';
+ await element.updateComplete;
+ await waitUntil(() => !suggestionsEl().isHidden);
+ assert.deepEqual(element.queryStatus, {
+ type: AutocompleteQueryStatusType.LOADING,
+ message: 'Loading...',
+ });
+
+ pressKey(inputEl(), Key.ENTER);
+ await element.updateComplete;
+ assert.deepEqual(element.queryStatus, {
+ type: AutocompleteQueryStatusType.LOADING,
+ message: 'Loading... (Handle Enter on load)',
+ });
+
+ element.text = 'more blah';
+ await element.updateComplete;
+
+ queryPromise.resolve([{name: 'suggestion 1'}] as AutocompleteSuggestion[]);
+ await element.latestSuggestionUpdateComplete;
+ await element.updateComplete;
+
+ // Commit for stale request is not called.
+ assert.isFalse(commitHandler.called);
+ });
+
+ test('while loading and queued esc cancels', async () => {
+ const commitHandler = sinon.stub();
+ element.addEventListener('commit', commitHandler);
+ const queryPromise = mockPromise<AutocompleteSuggestion[]>();
+ element.query = (_: string) => queryPromise;
+
+ element.setFocus(true);
+ element.text = 'blah';
+ await element.updateComplete;
+ await waitUntil(() => !suggestionsEl().isHidden);
+ assert.deepEqual(element.queryStatus, {
+ type: AutocompleteQueryStatusType.LOADING,
+ message: 'Loading...',
+ });
+
+ pressKey(inputEl(), Key.ENTER);
+ await element.updateComplete;
+ assert.deepEqual(element.queryStatus, {
+ type: AutocompleteQueryStatusType.LOADING,
+ message: 'Loading... (Handle Enter on load)',
+ });
+
+ pressKey(inputEl(), Key.ESC);
+ await element.updateComplete;
+
+ queryPromise.resolve([{name: 'suggestion 1'}] as AutocompleteSuggestion[]);
+ await element.latestSuggestionUpdateComplete;
+ await element.updateComplete;
+
+ // Commit for stale request is not called.
+ assert.isFalse(commitHandler.called);
+ // Query results and status are cleared
+ assert.equal(element.suggestions.length, 0);
+ assert.isUndefined(element.queryStatus);
+ });
+
suite('focus', () => {
let commitSpy: sinon.SinonSpy;
let focusSpy: sinon.SinonSpy;
@@ -688,13 +928,16 @@
await element.updateComplete;
assert.equal(element.suggestions.length, 0);
- assert.isUndefined(element.queryErrorMessage);
+ assert.isUndefined(element.queryStatus);
assert.isTrue(suggestionsEl().isHidden);
});
test('enter in input does not re-render error', async () => {
element.allowNonSuggestedValues = true;
- element.queryErrorMessage = 'Error message';
+ element.queryStatus = {
+ type: AutocompleteQueryStatusType.ERROR,
+ message: 'Error message',
+ };
pressKey(inputEl(), Key.ENTER);
@@ -702,7 +945,7 @@
await element.updateComplete;
assert.equal(element.suggestions.length, 0);
- assert.isUndefined(element.queryErrorMessage);
+ assert.isUndefined(element.queryStatus);
assert.isTrue(suggestionsEl().isHidden);
});
@@ -820,27 +1063,14 @@
});
});
- test('input-keydown event fired', async () => {
- const listener = sinon.spy();
- element.addEventListener('input-keydown', listener);
- pressKey(inputEl(), Key.TAB);
- await element.updateComplete;
- assert.isTrue(listener.called);
- });
-
test('enter with modifier does not complete', async () => {
- const dispatchEventStub = sinon.stub(element, 'dispatchEvent');
const commitStub = sinon.stub(element, 'handleInputCommit');
+
pressKey(inputEl(), Key.ENTER, Modifier.CTRL_KEY);
await element.updateComplete;
- assert.equal(dispatchEventStub.lastCall.args[0].type, 'input-keydown');
- assert.equal(
- (dispatchEventStub.lastCall.args[0] as CustomEvent).detail.key,
- Key.ENTER
- );
-
assert.isFalse(commitStub.called);
+
pressKey(inputEl(), Key.ENTER);
await element.updateComplete;
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 0bb451c..54fb825 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
@@ -14,6 +14,7 @@
import {resolve} from '../../../models/dependency';
import {shortcutsServiceToken} from '../../../services/shortcuts/shortcuts-service';
import {assertIsDefined} from '../../../utils/common-util';
+import {fire} from '../../../utils/event-util';
declare global {
interface HTMLElementTagNameMap {
@@ -93,12 +94,6 @@
change: this.change,
starred: newVal,
};
- this.dispatchEvent(
- new CustomEvent('toggle-star', {
- bubbles: true,
- composed: true,
- detail,
- })
- );
+ fire(this, 'toggle-star', detail);
}
}
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 c844d42..0edfe03 100644
--- a/polygerrit-ui/app/elements/shared/gr-comment/gr-comment.ts
+++ b/polygerrit-ui/app/elements/shared/gr-comment/gr-comment.ts
@@ -44,7 +44,7 @@
ReplyToCommentEventDetail,
ValueChangedEvent,
} from '../../../types/events';
-import {fire, fireEvent} from '../../../utils/event-util';
+import {fire} from '../../../utils/event-util';
import {assertIsDefined, assert} from '../../../utils/common-util';
import {Key, Modifier, whenVisible} from '../../../utils/dom-util';
import {commentsModelToken} from '../../../models/comments/comments-model';
@@ -275,6 +275,9 @@
this.save();
});
}
+ this.addEventListener('open-user-suggest-preview', e => {
+ this.handleShowFix(e.detail.code);
+ });
this.messagePlaceholder = 'Mention others with @';
subscribe(
this,
@@ -524,7 +527,6 @@
${this.renderCommentMessage()}
<gr-endpoint-slot name="above-actions"></gr-endpoint-slot>
${this.renderHumanActions()} ${this.renderRobotActions()}
- ${this.renderSuggestEditActions()}
</div>
</div>
</gr-endpoint-decorator>
@@ -776,32 +778,13 @@
return html`
<div class="rightActions">
${this.autoSaving ? html`. ` : ''}
- ${this.renderDiscardButton()} ${this.renderPreviewSuggestEditButton()}
- ${this.renderEditButton()} ${this.renderCancelButton()}
- ${this.renderSaveButton()} ${this.renderCopyLinkIcon()}
+ ${this.renderDiscardButton()} ${this.renderEditButton()}
+ ${this.renderCancelButton()} ${this.renderSaveButton()}
+ ${this.renderCopyLinkIcon()}
</div>
`;
}
- private renderPreviewSuggestEditButton() {
- if (!this.flagsService.isEnabled(KnownExperimentId.SUGGEST_EDIT)) {
- return nothing;
- }
- assertIsDefined(this.comment, 'comment');
- if (!hasUserSuggestion(this.comment)) return nothing;
- return html`
- <gr-button
- link
- secondary
- class="action show-fix"
- ?disabled=${this.saving}
- @click=${this.handleShowFix}
- >
- Preview Fix
- </gr-button>
- `;
- }
-
private renderSuggestEditButton() {
if (!this.flagsService.isEnabled(KnownExperimentId.SUGGEST_EDIT)) {
return nothing;
@@ -892,22 +875,6 @@
`;
}
- private renderSuggestEditActions() {
- if (!this.flagsService.isEnabled(KnownExperimentId.SUGGEST_EDIT)) {
- return nothing;
- }
- if (
- !this.account ||
- isRobot(this.comment) ||
- isDraftOrUnsaved(this.comment)
- ) {
- return nothing;
- }
- return html`
- <div class="robotActions">${this.renderPreviewSuggestEditButton()}</div>
- `;
- }
-
private renderShowFixButton() {
if (!(this.comment as RobotCommentInfo)?.fix_suggestions) return;
return html`
@@ -1015,7 +982,7 @@
}
private handleCopyLink() {
- fireEvent(this, 'copy-comment-link');
+ fire(this, 'copy-comment-link', {});
}
/** Enter editing mode. */
@@ -1037,12 +1004,14 @@
}
// private, but visible for testing
- async createFixPreview(): Promise<OpenFixPreviewEventDetail> {
+ async createFixPreview(
+ replacement?: string
+ ): Promise<OpenFixPreviewEventDetail> {
assertIsDefined(this.comment?.patch_set, 'comment.patch_set');
assertIsDefined(this.comment?.path, 'comment.path');
- if (hasUserSuggestion(this.comment)) {
- const replacement = getUserSuggestion(this.comment);
+ if (hasUserSuggestion(this.comment) || replacement) {
+ replacement = replacement ?? getUserSuggestion(this.comment);
assert(!!replacement, 'malformed user suggestion');
const line = await this.getCommentedCode();
@@ -1150,9 +1119,9 @@
fire(this, 'reply-to-comment', eventDetail);
}
- private async handleShowFix() {
+ private async handleShowFix(replacement?: string) {
// Handled top-level in the diff and change view components.
- fire(this, 'open-fix-preview', await this.createFixPreview());
+ fire(this, 'open-fix-preview', await this.createFixPreview(replacement));
}
async createSuggestEdit(e: MouseEvent) {
@@ -1316,4 +1285,7 @@
interface HTMLElementTagNameMap {
'gr-comment': GrComment;
}
+ interface HTMLElementEventMap {
+ 'copy-comment-link': CustomEvent<{}>;
+ }
}
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 3390369..6625844 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
@@ -29,19 +29,12 @@
import {
createComment,
createDraft,
- createFixSuggestionInfo,
createRobotComment,
createUnsaved,
} from '../../../test/test-data-generators';
-import {
- ReplyToCommentEvent,
- OpenFixPreviewEventDetail,
-} from '../../../types/events';
+import {ReplyToCommentEvent} from '../../../types/events';
import {GrConfirmDeleteCommentDialog} from '../gr-confirm-delete-comment-dialog/gr-confirm-delete-comment-dialog';
-import {
- DraftInfo,
- USER_SUGGESTION_START_PATTERN,
-} from '../../../utils/comment-util';
+import {DraftInfo} from '../../../utils/comment-util';
import {assertIsDefined} from '../../../utils/common-util';
import {Modifier} from '../../../utils/dom-util';
import {SinonStub} from 'sinon';
@@ -747,23 +740,6 @@
actions = query(element, '.robotActions gr-button.fix');
assert.isNotOk(actions);
});
-
- test('handleShowFix fires open-fix-preview event', async () => {
- const listener = listenOnce<CustomEvent<OpenFixPreviewEventDetail>>(
- element,
- 'open-fix-preview'
- );
- element.comment = {
- ...createRobotComment(),
- fix_suggestions: [{...createFixSuggestionInfo()}],
- };
- await element.updateComplete;
-
- queryAndAssert<GrButton>(element, '.show-fix').click();
-
- const e = await listener;
- assert.deepEqual(e.detail, await element.createFixPreview());
- });
});
suite('auto saving', () => {
@@ -869,33 +845,5 @@
</gr-button> `
);
});
-
- test('renders preview suggest fix', async () => {
- element.comment = {
- ...createComment(),
- author: {
- name: 'Mr. Peanutbutter',
- email: 'tenn1sballchaser@aol.com' as EmailAddress,
- },
- line: 5,
- path: 'test',
- message: `${USER_SUGGESTION_START_PATTERN}afterSuggestion${'\n```'}`,
- };
- await element.updateComplete;
-
- assert.dom.equal(
- queryAndAssert(element, 'gr-button.show-fix'),
- /* HTML */ `<gr-button
- aria-disabled="false"
- class="action show-fix"
- link=""
- role="button"
- secondary
- tabindex="0"
- >
- Preview Fix
- </gr-button> `
- );
- });
});
});
diff --git a/polygerrit-ui/app/elements/shared/gr-confirm-delete-comment-dialog/gr-confirm-delete-comment-dialog.ts b/polygerrit-ui/app/elements/shared/gr-confirm-delete-comment-dialog/gr-confirm-delete-comment-dialog.ts
index 285a41a..e8ab172 100644
--- a/polygerrit-ui/app/elements/shared/gr-confirm-delete-comment-dialog/gr-confirm-delete-comment-dialog.ts
+++ b/polygerrit-ui/app/elements/shared/gr-confirm-delete-comment-dialog/gr-confirm-delete-comment-dialog.ts
@@ -11,6 +11,7 @@
import {sharedStyles} from '../../../styles/shared-styles';
import {assertIsDefined} from '../../../utils/common-util';
import {BindValueChangeEvent} from '../../../types/events';
+import {fireNoBubble} from '../../../utils/event-util';
declare global {
interface HTMLElementTagNameMap {
@@ -108,23 +109,12 @@
private handleConfirmTap(e: Event) {
e.preventDefault();
e.stopPropagation();
- this.dispatchEvent(
- new CustomEvent('confirm', {
- detail: {reason: this.message},
- composed: true,
- bubbles: false,
- })
- );
+ fireNoBubble(this, 'confirm', {});
}
private handleCancelTap(e: Event) {
e.preventDefault();
e.stopPropagation();
- this.dispatchEvent(
- new CustomEvent('cancel', {
- composed: true,
- bubbles: false,
- })
- );
+ fireNoBubble(this, 'cancel', {});
}
}
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 350aa7f..8c280c0 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
@@ -66,7 +66,10 @@
color: var(--primary-text-color);
}
gr-icon {
- color: var(--deemphasized-text-color);
+ color: var(
+ --gr-copy-clipboard-icon-color,
+ var(--deemphasized-text-color)
+ );
}
gr-button {
display: block;
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 04d5923..6536806 100644
--- a/polygerrit-ui/app/elements/shared/gr-dialog/gr-dialog.ts
+++ b/polygerrit-ui/app/elements/shared/gr-dialog/gr-dialog.ts
@@ -10,6 +10,7 @@
import {sharedStyles} from '../../../styles/shared-styles';
import {fontStyles} from '../../../styles/gr-font-styles';
import {when} from 'lit/directives/when.js';
+import {fireNoBubble} from '../../../utils/event-util';
declare global {
interface HTMLElementTagNameMap {
@@ -199,23 +200,13 @@
e.preventDefault();
e.stopPropagation();
- this.dispatchEvent(
- new CustomEvent('confirm', {
- composed: true,
- bubbles: false,
- })
- );
+ fireNoBubble(this, 'confirm', {});
}
private handleCancelTap(e: Event) {
e.preventDefault();
e.stopPropagation();
- this.dispatchEvent(
- new CustomEvent('cancel', {
- composed: true,
- bubbles: false,
- })
- );
+ fireNoBubble(this, 'cancel', {});
}
_handleKeydown(e: KeyboardEvent) {
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 b6ca9f5..c935e72 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
@@ -23,6 +23,7 @@
import {incrementalRepeat} from '../../lit/incremental-repeat';
import {when} from 'lit/directives/when.js';
import {isMagicPath} from '../../../utils/path-list-util';
+import {fireNoBubble} from '../../../utils/event-util';
/**
* Required values are text and value. mobileText and triggerText will
@@ -303,12 +304,7 @@
this.text = selectedObj.triggerText
? selectedObj.triggerText
: selectedObj.text;
- this.dispatchEvent(
- new CustomEvent('value-change', {
- detail: {value: this.value},
- bubbles: false,
- })
- );
+ fireNoBubble(this, 'value-change', {value: this.value});
}
/**
diff --git a/polygerrit-ui/app/elements/shared/gr-dropdown/gr-dropdown.ts b/polygerrit-ui/app/elements/shared/gr-dropdown/gr-dropdown.ts
index 3a8946a..495b448 100644
--- a/polygerrit-ui/app/elements/shared/gr-dropdown/gr-dropdown.ts
+++ b/polygerrit-ui/app/elements/shared/gr-dropdown/gr-dropdown.ts
@@ -242,7 +242,8 @@
allowOutsideScroll
.horizontalAlign=${this.horizontalAlign}
@click=${() => this.close()}
- @opened-changed=${(e: CustomEvent) => (this.opened = e.detail.value)}
+ @opened-changed=${(e: ValueChangedEvent<boolean>) =>
+ (this.opened = e.detail.value)}
>
${this.renderDropdownContent()}
</iron-dropdown>`;
@@ -460,13 +461,7 @@
const item = this.items.find(item => item.id === id);
if (id && !this.disabledIds.includes(id)) {
if (item) {
- this.dispatchEvent(
- new CustomEvent('tap-item', {
- detail: item,
- bubbles: true,
- composed: true,
- })
- );
+ fire(this, 'tap-item', item);
}
this.dispatchEvent(new CustomEvent('tap-item-' + id));
}
diff --git a/polygerrit-ui/app/elements/shared/gr-editable-content/gr-editable-content.ts b/polygerrit-ui/app/elements/shared/gr-editable-content/gr-editable-content.ts
index e176598..3110a96 100644
--- a/polygerrit-ui/app/elements/shared/gr-editable-content/gr-editable-content.ts
+++ b/polygerrit-ui/app/elements/shared/gr-editable-content/gr-editable-content.ts
@@ -10,7 +10,7 @@
import '../../plugins/gr-endpoint-decorator/gr-endpoint-decorator';
import '../../plugins/gr-endpoint-param/gr-endpoint-param';
import '../../plugins/gr-endpoint-slot/gr-endpoint-slot';
-import {fire, fireAlert, fireEvent} from '../../../utils/event-util';
+import {fire, fireAlert} from '../../../utils/event-util';
import {getAppContext} from '../../../services/app-context';
import {debounce, DelayedTask} from '../../../utils/async-util';
import {queryAndAssert} from '../../../utils/common-util';
@@ -21,7 +21,11 @@
import {sharedStyles} from '../../../styles/shared-styles';
import {css} from 'lit';
import {PropertyValues} from 'lit';
-import {BindValueChangeEvent, ValueChangedEvent} from '../../../types/events';
+import {
+ BindValueChangeEvent,
+ EditableContentSaveEvent,
+ ValueChangedEvent,
+} from '../../../types/events';
import {nothing} from 'lit';
import {classMap} from 'lit/directives/class-map.js';
import {when} from 'lit/directives/when.js';
@@ -39,24 +43,16 @@
interface HTMLElementEventMap {
'content-changed': ValueChangedEvent<string>;
'editing-changed': ValueChangedEvent<boolean>;
+ /** Fired when the 'cancel' button is pressed. */
+ 'editable-content-cancel': CustomEvent<{}>;
+ /** Fired when the 'save' button is pressed. */
+ 'editable-content-save': EditableContentSaveEvent;
}
}
@customElement('gr-editable-content')
export class GrEditableContent extends LitElement {
/**
- * Fired when the save button is pressed.
- *
- * @event editable-content-save
- */
-
- /**
- * Fired when the cancel button is pressed.
- *
- * @event editable-content-cancel
- */
-
- /**
* Fired when content is restored from storage.
*
* @event show-alert
@@ -386,13 +382,7 @@
handleSave(e: Event) {
e.preventDefault();
- this.dispatchEvent(
- new CustomEvent('editable-content-save', {
- detail: {content: this.newContent},
- composed: true,
- bubbles: true,
- })
- );
+ fire(this, 'editable-content-save', {content: this.newContent});
// It would be nice, if we would set this.newContent = undefined here,
// but we can only do that when we are sure that the save operation has
// succeeded.
@@ -401,7 +391,7 @@
handleCancel(e: Event) {
e.preventDefault();
this.editing = false;
- fireEvent(this, 'editable-content-cancel');
+ fire(this, 'editable-content-cancel', {});
}
toggleCommitCollapsed() {
diff --git a/polygerrit-ui/app/elements/shared/gr-editable-content/gr-editable-content_test.ts b/polygerrit-ui/app/elements/shared/gr-editable-content/gr-editable-content_test.ts
index b4f25ce..4a5611b 100644
--- a/polygerrit-ui/app/elements/shared/gr-editable-content/gr-editable-content_test.ts
+++ b/polygerrit-ui/app/elements/shared/gr-editable-content/gr-editable-content_test.ts
@@ -9,7 +9,6 @@
import {query, queryAndAssert} from '../../../test/test-utils';
import {GrButton} from '../gr-button/gr-button';
import {fixture, html, assert} from '@open-wc/testing';
-import {EventType} from '../../../types/events';
import {StorageService} from '../../../services/storage/gr-storage';
import {storageServiceToken} from '../../../services/storage/gr-storage_impl';
import {testResolver} from '../../../test/common-test-setup';
@@ -190,7 +189,7 @@
await element.updateComplete;
assert.equal(element.newContent, 'stored content');
assert.isTrue(dispatchSpy.called);
- assert.equal(dispatchSpy.lastCall.args[0].type, EventType.SHOW_ALERT);
+ assert.equal(dispatchSpy.lastCall.args[0].type, 'show-alert');
});
test('editing toggled to true, has no stored data', async () => {
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 bf8209b..c8574cd 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
@@ -21,6 +21,8 @@
import {PaperInputElement} from '@polymer/paper-input/paper-input';
import {IronInputElement} from '@polymer/iron-input';
import {ShortcutController} from '../../lit/shortcut-controller';
+import {ValueChangedEvent} from '../../../types/events';
+import {fire} from '../../../utils/event-util';
const AWAIT_MAX_ITERS = 10;
const AWAIT_STEP = 5;
@@ -207,7 +209,7 @@
.text=${this.inputText}
.query=${this.query}
@cancel=${this.cancel}
- @text-changed=${(e: CustomEvent) => {
+ @text-changed=${(e: ValueChangedEvent) => {
this.inputText = e.detail.value;
}}
>
@@ -308,13 +310,8 @@
this.value = this.inputText || '';
}
this.editing = false;
- this.dispatchEvent(
- new CustomEvent('changed', {
- detail: this.value,
- composed: true,
- bubbles: true,
- })
- );
+ // TODO: This event seems to be unused (no listener). Remove?
+ fire(this, 'changed', this.value);
}
private cancel() {
diff --git a/polygerrit-ui/app/elements/shared/gr-editable-label/gr-editable-label_test.ts b/polygerrit-ui/app/elements/shared/gr-editable-label/gr-editable-label_test.ts
index d916118..3bb058e 100644
--- a/polygerrit-ui/app/elements/shared/gr-editable-label/gr-editable-label_test.ts
+++ b/polygerrit-ui/app/elements/shared/gr-editable-label/gr-editable-label_test.ts
@@ -295,7 +295,10 @@
suggestions = [{name: 'value text 1'}, {name: 'value text 2'}];
await element.open();
+ // Waiting until dropdown not hidden, will ensure dialog is open and input
+ // is focused, but not that the suggestion has loaded.
await waitUntil(() => !autocomplete.suggestionsDropdown!.isHidden);
+ await autocomplete.latestSuggestionUpdateComplete;
pressKey(autocomplete.input!, Key.ENTER);
@@ -312,7 +315,11 @@
test('autocomplete suggestions closed enter saves suggestion', async () => {
suggestions = [{name: 'value text 1'}, {name: 'value text 2'}];
await element.open();
+ // Waiting until dropdown not hidden, will ensure dialog is open and input
+ // is focused, but not that the suggestion has loaded.
await waitUntil(() => !autocomplete.suggestionsDropdown!.isHidden);
+ await autocomplete.latestSuggestionUpdateComplete;
+
// Press enter to close suggestions.
pressKey(autocomplete.input!, Key.ENTER);
diff --git a/polygerrit-ui/app/elements/shared/gr-formatted-text/gr-formatted-text.ts b/polygerrit-ui/app/elements/shared/gr-formatted-text/gr-formatted-text.ts
index 45eca40..023d8b5 100644
--- a/polygerrit-ui/app/elements/shared/gr-formatted-text/gr-formatted-text.ts
+++ b/polygerrit-ui/app/elements/shared/gr-formatted-text/gr-formatted-text.ts
@@ -18,6 +18,10 @@
import {CommentLinks, EmailAddress} from '../../../api/rest-api';
import {linkifyUrlsAndApplyRewrite} from '../../../utils/link-util';
import '../gr-account-chip/gr-account-chip';
+import '../gr-user-suggestion-fix/gr-user-suggestion-fix';
+import {KnownExperimentId} from '../../../services/flags/flags';
+import {getAppContext} from '../../../services/app-context';
+import {USER_SUGGESTION_INFO_STRING} from '../../../utils/comment-util';
/**
* This element optionally renders markdown and also applies some regex
@@ -34,6 +38,8 @@
@state()
private repoCommentLinks: CommentLinks = {};
+ private readonly flagsService = getAppContext().flagsService;
+
private readonly getConfigModel = resolve(this, configModelToken);
// Private const but used in tests.
@@ -134,6 +140,10 @@
}
private renderAsMarkdown() {
+ // need to find out here, since customRender is not arrow function
+ const suggestEditsEnable = this.flagsService.isEnabled(
+ KnownExperimentId.SUGGEST_EDIT
+ );
// <marked-element> internals will be in charge of calling our custom
// renderer so we wrap 'this.rewriteText' so that 'this' is preserved via
// closure.
@@ -167,7 +177,18 @@
`![${text}](${href})`;
renderer['codespan'] = (text: string) =>
`<code>${unescapeHTML(text)}</code>`;
- renderer['code'] = (text: string) => `<pre><code>${text}</code></pre>`;
+ renderer['code'] = (text: string, infostring: string) => {
+ if (suggestEditsEnable && infostring === USER_SUGGESTION_INFO_STRING) {
+ // default santizer in markedjs is very restrictive, we need to use
+ // existing html element to mark element. We cannot use css class for it.
+ // Therefore we pick mark - as not frequently used html element to represent
+ // unconverted gr-user-suggestion-fix.
+ // TODO(milutin): Find a way to override sanitizer to directly use gr-user-suggestion-fix
+ return `<mark>${text}</mark>`;
+ } else {
+ return `<pre><code>${text}</code></pre>`;
+ }
+ };
renderer['text'] = boundRewriteText;
}
@@ -211,6 +232,9 @@
override updated() {
// Look for @mentions and replace them with an account-label chip.
this.convertEmailsToAccountChips();
+ if (this.flagsService.isEnabled(KnownExperimentId.SUGGEST_EDIT)) {
+ this.convertCodeToSuggestions();
+ }
}
private convertEmailsToAccountChips() {
@@ -235,6 +259,17 @@
}
}
}
+
+ private convertCodeToSuggestions() {
+ for (const userSuggestionMark of this.renderRoot.querySelectorAll('mark')) {
+ const userSuggestion = document.createElement('gr-user-suggestion-fix');
+ userSuggestion.textContent = userSuggestionMark.textContent ?? '';
+ userSuggestionMark.parentNode?.replaceChild(
+ userSuggestion,
+ userSuggestionMark
+ );
+ }
+ }
}
declare global {
diff --git a/polygerrit-ui/app/elements/shared/gr-formatted-text/gr-formatted-text_test.ts b/polygerrit-ui/app/elements/shared/gr-formatted-text/gr-formatted-text_test.ts
index fcebeea..0e5117a 100644
--- a/polygerrit-ui/app/elements/shared/gr-formatted-text/gr-formatted-text_test.ts
+++ b/polygerrit-ui/app/elements/shared/gr-formatted-text/gr-formatted-text_test.ts
@@ -587,5 +587,25 @@
`
);
});
+
+ suite('user suggest fix', () => {
+ setup(async () => {
+ const flagsService = getAppContext().flagsService;
+ sinon.stub(flagsService, 'isEnabled').returns(true);
+ });
+
+ test('renders', async () => {
+ element.content = '```suggestion\nHello World```';
+ await element.updateComplete;
+ assert.shadowDom.equal(
+ element,
+ /* HTML */ `<marked-element>
+ <div class="markdown-html" slot="markdown-html">
+ <gr-user-suggestion-fix>Hello World</gr-user-suggestion-fix>
+ </div>
+ </marked-element>`
+ );
+ });
+ });
});
});
diff --git a/polygerrit-ui/app/elements/shared/gr-hovercard-account/gr-hovercard-account-contents.ts b/polygerrit-ui/app/elements/shared/gr-hovercard-account/gr-hovercard-account-contents.ts
index 2730fcf..14c2c15 100644
--- a/polygerrit-ui/app/elements/shared/gr-hovercard-account/gr-hovercard-account-contents.ts
+++ b/polygerrit-ui/app/elements/shared/gr-hovercard-account/gr-hovercard-account-contents.ts
@@ -38,13 +38,12 @@
import {sharedStyles} from '../../../styles/shared-styles';
import {css, html, LitElement, nothing} from 'lit';
import {ifDefined} from 'lit/directives/if-defined.js';
-import {EventType} from '../../../types/events';
import {subscribe} from '../../lit/subscription-controller';
import {resolve} from '../../../models/dependency';
import {configModelToken} from '../../../models/config/config-model';
import {createSearchUrl} from '../../../models/views/search';
import {createDashboardUrl} from '../../../models/views/dashboard';
-import {fire, fireEvent} from '../../../utils/event-util';
+import {fire} from '../../../utils/event-util';
import {userModelToken} from '../../../models/user/user-model';
@customElement('gr-hovercard-account-contents')
@@ -251,10 +250,10 @@
<a
href=${ifDefined(this.computeOwnerChangesLink())}
@click=${() => {
- fireEvent(this, 'link-clicked');
+ fire(this, 'link-clicked', {});
}}
@enter=${() => {
- fireEvent(this, 'link-clicked');
+ fire(this, 'link-clicked', {});
}}
>
Changes
@@ -263,10 +262,10 @@
<a
href=${ifDefined(this.computeOwnerDashboardLink())}
@click=${() => {
- fireEvent(this, 'link-clicked');
+ fire(this, 'link-clicked', {});
}}
@enter=${() => {
- fireEvent(this, 'link-clicked');
+ fire(this, 'link-clicked', {});
}}
>
Dashboard
@@ -423,7 +422,7 @@
// accountKey() throws an error if _account_id & email is not found, which
// we want to check before showing reloading toast
const _accountKey = accountKey(this.account);
- fire(this, EventType.SHOW_ALERT, {
+ fire(this, 'show-alert', {
message: 'Reloading page...',
});
const reviewInput: Partial<ReviewInput> = {};
@@ -453,7 +452,7 @@
private handleRemoveReviewerOrCC() {
if (!this.change || !(this.account?._account_id || this.account?.email))
throw new Error('Missing change or account.');
- fire(this, EventType.SHOW_ALERT, {
+ fire(this, 'show-alert', {
message: 'Reloading page...',
});
this.restApiService
@@ -486,7 +485,7 @@
private handleClickAddToAttentionSet() {
if (!this.change || !this.account._account_id) return;
- fire(this, EventType.SHOW_ALERT, {
+ fire(this, 'show-alert', {
message: 'Reloading page...',
dismissOnNavigation: true,
});
@@ -501,7 +500,7 @@
reason,
reason_account: this.selfAccount,
};
- fireEvent(this, 'attention-set-updated');
+ fire(this, 'attention-set-updated', {});
this.reporting.reportInteraction(
'attention-hovercard-add',
@@ -510,14 +509,14 @@
this.restApiService
.addToAttentionSet(this.change._number, this.account._account_id, reason)
.then(() => {
- fireEvent(this, 'hide-alert');
+ fire(this, 'hide-alert', {});
});
- fireEvent(this, 'action-taken');
+ fire(this, 'action-taken', {});
}
private handleClickRemoveFromAttentionSet() {
if (!this.change || !this.account._account_id) return;
- fire(this, EventType.SHOW_ALERT, {
+ fire(this, 'show-alert', {
message: 'Saving attention set update ...',
dismissOnNavigation: true,
});
@@ -528,7 +527,7 @@
const reason = getRemovedByReason(this.selfAccount, this.serverConfig);
if (this.change.attention_set)
delete this.change.attention_set[this.account._account_id];
- fireEvent(this, 'attention-set-updated');
+ fire(this, 'attention-set-updated', {});
this.reporting.reportInteraction(
'attention-hovercard-remove',
@@ -541,9 +540,9 @@
reason
)
.then(() => {
- fireEvent(this, 'hide-alert');
+ fire(this, 'hide-alert', {});
});
- fireEvent(this, 'action-taken');
+ fire(this, 'action-taken', {});
}
private reportingDetails() {
@@ -572,4 +571,9 @@
interface HTMLElementTagNameMap {
'gr-hovercard-account-contents': GrHovercardAccountContents;
}
+ interface HTMLElementEventMap {
+ 'action-taken': CustomEvent<{}>;
+ 'attention-set-updated': CustomEvent<{}>;
+ 'link-clicked': CustomEvent<{}>;
+ }
}
diff --git a/polygerrit-ui/app/elements/shared/gr-hovercard-account/gr-hovercard-account-contents_test.ts b/polygerrit-ui/app/elements/shared/gr-hovercard-account/gr-hovercard-account-contents_test.ts
index bd47ec8..7df06f4 100644
--- a/polygerrit-ui/app/elements/shared/gr-hovercard-account/gr-hovercard-account-contents_test.ts
+++ b/polygerrit-ui/app/elements/shared/gr-hovercard-account/gr-hovercard-account-contents_test.ts
@@ -26,7 +26,6 @@
createDetailedLabelInfo,
} from '../../../test/test-data-generators';
import {GrButton} from '../gr-button/gr-button';
-import {EventType} from '../../../types/events';
import {testResolver} from '../../../test/common-test-setup';
import {userModelToken} from '../../../models/user/user-model';
@@ -308,7 +307,7 @@
const showAlertListener = sinon.spy();
const hideAlertListener = sinon.spy();
const updatedListener = sinon.spy();
- element.addEventListener(EventType.SHOW_ALERT, showAlertListener);
+ element.addEventListener('show-alert', showAlertListener);
element.addEventListener('hide-alert', hideAlertListener);
element.addEventListener('attention-set-updated', updatedListener);
@@ -359,7 +358,7 @@
const showAlertListener = sinon.spy();
const hideAlertListener = sinon.spy();
const updatedListener = sinon.spy();
- element.addEventListener(EventType.SHOW_ALERT, showAlertListener);
+ element.addEventListener('show-alert', showAlertListener);
element.addEventListener('hide-alert', hideAlertListener);
element.addEventListener('attention-set-updated', updatedListener);
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-action-context.ts b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-action-context.ts
index 0628d2f..c81f586 100644
--- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-action-context.ts
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-action-context.ts
@@ -4,13 +4,13 @@
* SPDX-License-Identifier: Apache-2.0
*/
import {RevisionInfo, ChangeInfo, RequestPayload} from '../../../types/common';
-import {EventType, ShowAlertEventDetail} from '../../../types/events';
import {PluginApi} from '../../../api/plugin';
import {UIActionInfo} from './gr-change-actions-js-api';
import {windowLocationReload} from '../../../utils/dom-util';
import {PopupPluginApi} from '../../../api/popup';
import {GrPopupInterface} from '../../plugins/gr-popup-interface/gr-popup-interface';
import {getAppContext} from '../../../services/app-context';
+import {fireAlert} from '../../../utils/event-util';
interface ButtonCallBacks {
onclick: (event: Event) => boolean;
@@ -110,13 +110,7 @@
.send(this.action.method, this.action.__url, payload)
.then(onSuccess)
.catch((error: unknown) => {
- document.dispatchEvent(
- new CustomEvent<ShowAlertEventDetail>(EventType.SHOW_ALERT, {
- detail: {
- message: `Plugin network error: ${error}`,
- },
- })
- );
+ fireAlert(document, `Plugin network error: ${error}`);
});
}
}
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-action-context_test.ts b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-action-context_test.ts
index eaf6612..76a6573 100644
--- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-action-context_test.ts
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-action-context_test.ts
@@ -7,7 +7,6 @@
import './gr-js-api-interface';
import {GrPluginActionContext} from './gr-plugin-action-context';
import {addListenerForTest, waitEventLoop} from '../../../test/test-utils';
-import {EventType} from '../../../types/events';
import {assert} from '@open-wc/testing';
import {PluginApi} from '../../../api/plugin';
import {SinonStub, stub, spy} from 'sinon';
@@ -147,7 +146,7 @@
} as unknown as RestPluginApi;
stub(plugin, 'restApi').returns(fakeRestApi);
const errorStub = stub();
- addListenerForTest(document, EventType.SHOW_ALERT, errorStub);
+ addListenerForTest(document, 'show-alert', errorStub);
instance.call({}, () => {});
await waitEventLoop();
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-loader_test.ts b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-loader_test.ts
index acce236..b2ac2bf 100644
--- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-loader_test.ts
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-loader_test.ts
@@ -11,7 +11,6 @@
import {PluginApi} from '../../../api/plugin';
import {SinonFakeTimers} from 'sinon';
import {Timestamp} from '../../../api/rest-api';
-import {EventType} from '../../../types/events';
import {assert} from '@open-wc/testing';
import {getAppContext} from '../../../services/app-context';
@@ -144,7 +143,7 @@
];
const alertStub = sinon.stub();
- addListenerForTest(document, EventType.SHOW_ALERT, alertStub);
+ addListenerForTest(document, 'show-alert', alertStub);
sinon.stub(pluginLoader, 'loadJsPlugin').callsFake(url => {
pluginLoader.install(
@@ -178,7 +177,7 @@
];
const alertStub = sinon.stub();
- addListenerForTest(document, EventType.SHOW_ALERT, alertStub);
+ addListenerForTest(document, 'show-alert', alertStub);
sinon.stub(pluginLoader, 'loadJsPlugin').callsFake(url => {
pluginLoader.install(
@@ -217,7 +216,7 @@
];
const alertStub = sinon.stub();
- addListenerForTest(document, EventType.SHOW_ALERT, alertStub);
+ addListenerForTest(document, 'show-alert', alertStub);
sinon.stub(pluginLoader, 'loadJsPlugin').callsFake(url => {
pluginLoader.install(
@@ -249,7 +248,7 @@
];
const alertStub = sinon.stub();
- addListenerForTest(document, EventType.SHOW_ALERT, alertStub);
+ addListenerForTest(document, 'show-alert', alertStub);
sinon.stub(pluginLoader, 'loadJsPlugin').callsFake(url => {
pluginLoader.install(() => {}, url === plugins[0] ? '' : 'alpha', url);
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 47d722d..02f830d 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,7 +22,7 @@
import {customElement, property} from 'lit/decorators.js';
import {GrButton} from '../gr-button/gr-button';
import {
- canVote,
+ canReviewerVote,
getApprovalInfo,
hasNeutralStatus,
hasVoted,
@@ -143,7 +143,7 @@
.filter(reviewer => {
if (this.showAllReviewers) {
if (isDetailedLabelInfo(labelInfo)) {
- return canVote(labelInfo, reviewer);
+ return canReviewerVote(labelInfo, reviewer);
} else {
// isQuickLabelInfo
return hasVoted(labelInfo, reviewer);
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 e48dcb3..7717683 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
@@ -6,7 +6,7 @@
import '../gr-button/gr-button';
import '../gr-icon/gr-icon';
import '../gr-limited-text/gr-limited-text';
-import {fireEvent} from '../../../utils/event-util';
+import {fire} from '../../../utils/event-util';
import {sharedStyles} from '../../../styles/shared-styles';
import {LitElement, css, html} from 'lit';
import {customElement, property} from 'lit/decorators.js';
@@ -15,6 +15,11 @@
interface HTMLElementTagNameMap {
'gr-linked-chip': GrLinkedChip;
}
+ interface HTMLElementEventMap {
+ /** Fired when the 'remove' button was clicked. */
+ // prettier-ignore
+ 'remove': CustomEvent<{}>;
+ }
}
@customElement('gr-linked-chip')
@@ -101,6 +106,6 @@
private handleRemoveTap(e: Event) {
e.preventDefault();
- fireEvent(this, 'remove');
+ fire(this, 'remove', {});
}
}
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 7e90c8d..ea45142 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
@@ -7,7 +7,7 @@
import '../gr-button/gr-button';
import '../gr-icon/gr-icon';
import {encodeURL, getBaseUrl} from '../../../utils/url-util';
-import {fireEvent} from '../../../utils/event-util';
+import {fire} from '../../../utils/event-util';
import {debounce, DelayedTask} from '../../../utils/async-util';
import {sharedStyles} from '../../../styles/shared-styles';
import {LitElement, PropertyValues, css, html} from 'lit';
@@ -22,6 +22,9 @@
interface HTMLElementTagNameMap {
'gr-list-view': GrListView;
}
+ interface HTMLElementEventMap {
+ 'create-clicked': CustomEvent<{}>;
+ }
}
@customElement('gr-list-view')
@@ -180,7 +183,7 @@
}
private createNewItem() {
- fireEvent(this, 'create-clicked');
+ fire(this, 'create-clicked', {});
}
// private but used in test
diff --git a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-apis/gr-rest-api-helper.ts b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-apis/gr-rest-api-helper.ts
index 8b3e2a6..615a04a 100644
--- a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-apis/gr-rest-api-helper.ts
+++ b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-apis/gr-rest-api-helper.ts
@@ -17,7 +17,11 @@
} from '../../../../types/common';
import {HttpMethod} from '../../../../constants/constants';
import {RpcLogEventDetail} from '../../../../types/events';
-import {fireNetworkError, fireServerError} from '../../../../utils/event-util';
+import {
+ fire,
+ fireNetworkError,
+ fireServerError,
+} from '../../../../utils/event-util';
import {FetchRequest} from '../../../../types/types';
import {ErrorCallback} from '../../../../api/rest';
import {Scheduler, Task} from '../../../../services/scheduler/scheduler';
@@ -337,13 +341,7 @@
elapsed,
anonymizedUrl: req.anonymizedUrl,
};
- document.dispatchEvent(
- new CustomEvent('gr-rpc-log', {
- detail,
- composed: true,
- bubbles: true,
- })
- );
+ fire(document, 'gr-rpc-log', detail);
}
}
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 20a6463..782c055 100644
--- a/polygerrit-ui/app/elements/shared/gr-textarea/gr-textarea.ts
+++ b/polygerrit-ui/app/elements/shared/gr-textarea/gr-textarea.ts
@@ -12,7 +12,7 @@
import {
GrAutocompleteDropdown,
Item,
- ItemSelectedEvent,
+ ItemSelectedEventDetail,
} from '../gr-autocomplete-dropdown/gr-autocomplete-dropdown';
import {Key} from '../../../utils/dom-util';
import {ValueChangedEvent} from '../../../types/events';
@@ -67,7 +67,7 @@
declare global {
interface HTMLElementEventMap {
- 'item-selected': CustomEvent<ItemSelectedEvent>;
+ 'item-selected': CustomEvent<ItemSelectedEventDetail>;
}
}
@@ -375,7 +375,7 @@
}
// private but used in test
- handleDropdownItemSelect(e: CustomEvent<ItemSelectedEvent>) {
+ handleDropdownItemSelect(e: CustomEvent<ItemSelectedEventDetail>) {
if (e.detail.selected?.dataset['value']) {
this.setValue(e.detail.selected?.dataset['value']);
}
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 711866a..154ea0a 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
@@ -6,7 +6,7 @@
import '../../../test/common-test-setup';
import './gr-textarea';
import {GrTextarea} from './gr-textarea';
-import {ItemSelectedEvent} from '../gr-autocomplete-dropdown/gr-autocomplete-dropdown';
+import {ItemSelectedEventDetail} from '../gr-autocomplete-dropdown/gr-autocomplete-dropdown';
import {pressKey, stubRestApi, waitUntil} from '../../../test/test-utils';
import {fixture, html, assert} from '@open-wc/testing';
import {createAccountWithEmail} from '../../../test/test-data-generators';
@@ -482,7 +482,7 @@
element.specialCharIndex = 10;
await element.updateComplete;
const selectedItem = {dataset: {value: '😂'}} as unknown as HTMLElement;
- const event = new CustomEvent<ItemSelectedEvent>('item-selected', {
+ const event = new CustomEvent<ItemSelectedEventDetail>('item-selected', {
detail: {trigger: 'click', selected: selectedItem},
});
element.handleDropdownItemSelect(event);
diff --git a/polygerrit-ui/app/elements/shared/gr-user-suggestion-fix/gr-user-suggestion-fix.ts b/polygerrit-ui/app/elements/shared/gr-user-suggestion-fix/gr-user-suggestion-fix.ts
new file mode 100644
index 0000000..c557acc
--- /dev/null
+++ b/polygerrit-ui/app/elements/shared/gr-user-suggestion-fix/gr-user-suggestion-fix.ts
@@ -0,0 +1,97 @@
+/**
+ * @license
+ * Copyright 2023 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+import {css, html, LitElement, nothing} from 'lit';
+import {customElement} from 'lit/decorators.js';
+import {getAppContext} from '../../../services/app-context';
+import {KnownExperimentId} from '../../../services/flags/flags';
+import {fire} from '../../../utils/event-util';
+
+declare global {
+ interface HTMLElementEventMap {
+ 'open-user-suggest-preview': OpenUserSuggestionPreviewEvent;
+ }
+}
+
+export type OpenUserSuggestionPreviewEvent =
+ CustomEvent<OpenUserSuggestionPreviewEventDetail>;
+export interface OpenUserSuggestionPreviewEventDetail {
+ code: string;
+}
+
+@customElement('gr-user-suggestion-fix')
+export class GrUserSuggetionFix extends LitElement {
+ private readonly flagsService = getAppContext().flagsService;
+
+ static override styles = [
+ css`
+ .header {
+ background-color: var(--user-suggestion-header-background);
+ color: var(--user-suggestion-header-color);
+ border: 1px solid var(--border-color);
+ border-bottom: 0;
+ padding: var(--spacing-xs) var(--spacing-s);
+ display: flex;
+ align-items: center;
+ }
+ .header .title {
+ flex: 1;
+ }
+ gr-copy-clipboard {
+ --gr-copy-clipboard-icon-color: var(--user-suggestion-header-color);
+ }
+ code {
+ max-width: var(--gr-formatted-text-prose-max-width, none);
+ background-color: var(--background-color-secondary);
+ border: 1px solid var(--border-color);
+ border-top: 0;
+ display: block;
+ font-family: var(--monospace-font-family);
+ font-size: var(--font-size-code);
+ line-height: var(--line-height-mono);
+ margin-bottom: var(--spacing-m);
+ padding: var(--spacing-xxs) var(--spacing-s);
+ overflow-x: auto;
+ /* Pre will preserve whitespace and line breaks but not wrap */
+ white-space: pre;
+ }
+ `,
+ ];
+
+ override render() {
+ if (!this.flagsService.isEnabled(KnownExperimentId.SUGGEST_EDIT)) {
+ return nothing;
+ }
+ if (!this.textContent) return nothing;
+ const code = this.textContent;
+ return html`<div class="header">
+ <div class="title">Suggested fix</div>
+ <div>
+ <gr-copy-clipboard hideInput="" text=${code}></gr-copy-clipboard>
+ </div>
+ <div>
+ <gr-button
+ secondary
+ class="action show-fix"
+ @click=${this.handleShowFix}
+ >
+ Preview Fix
+ </gr-button>
+ </div>
+ </div>
+ <code>${code}</code>`;
+ }
+
+ handleShowFix() {
+ if (!this.textContent) return;
+ fire(this, 'open-user-suggest-preview', {code: this.textContent});
+ }
+}
+
+declare global {
+ interface HTMLElementTagNameMap {
+ 'gr-user-suggestion-fix': GrUserSuggetionFix;
+ }
+}
diff --git a/polygerrit-ui/app/elements/shared/gr-user-suggestion-fix/gr-user-suggestion-fix_test.ts b/polygerrit-ui/app/elements/shared/gr-user-suggestion-fix/gr-user-suggestion-fix_test.ts
new file mode 100644
index 0000000..80422a0
--- /dev/null
+++ b/polygerrit-ui/app/elements/shared/gr-user-suggestion-fix/gr-user-suggestion-fix_test.ts
@@ -0,0 +1,46 @@
+/**
+ * @license
+ * Copyright 2023 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+import '../../../test/common-test-setup';
+import './gr-user-suggestion-fix';
+import {fixture, html, assert} from '@open-wc/testing';
+import {GrUserSuggetionFix} from './gr-user-suggestion-fix';
+import {getAppContext} from '../../../services/app-context';
+
+suite('gr-user-suggestion-fix tests', () => {
+ let element: GrUserSuggetionFix;
+
+ setup(async () => {
+ const flagsService = getAppContext().flagsService;
+ sinon.stub(flagsService, 'isEnabled').returns(true);
+ element = await fixture<GrUserSuggetionFix>(html`
+ <gr-user-suggestion-fix>Hello World</gr-user-suggestion-fix>
+ `);
+ await element.updateComplete;
+ });
+
+ test('render', async () => {
+ await element.updateComplete;
+
+ assert.shadowDom.equal(
+ element,
+ /* HTML */ `<div class="header">
+ <div class="title">Suggested fix</div>
+ <div>
+ <gr-copy-clipboard
+ hideinput=""
+ text="Hello World"
+ ></gr-copy-clipboard>
+ </div>
+ <div>
+ <gr-button class="action show-fix" secondary=""
+ >Preview Fix</gr-button
+ >
+ </div>
+ </div>
+ <code>Hello World</code>`
+ );
+ });
+});
diff --git a/polygerrit-ui/app/embed/diff/gr-context-controls/gr-context-controls.ts b/polygerrit-ui/app/embed/diff/gr-context-controls/gr-context-controls.ts
index 1679ee0..4a2fee5 100644
--- a/polygerrit-ui/app/embed/diff/gr-context-controls/gr-context-controls.ts
+++ b/polygerrit-ui/app/embed/diff/gr-context-controls/gr-context-controls.ts
@@ -161,6 +161,10 @@
/* same as defined in gr-button */
background: rgba(0, 0, 0, 0.12);
}
+ paper-button:focus-visible {
+ /* paper-button sets this to 0, thus preventing focus-based styling. */
+ outline-width: 1px;
+ }
.aboveBelowButtons {
display: flex;
diff --git a/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-builder-element.ts b/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-builder-element.ts
index 12d7ec2..71a0637 100644
--- a/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-builder-element.ts
+++ b/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-builder-element.ts
@@ -41,7 +41,7 @@
hideInContextControl,
} from '../gr-diff/gr-diff-group';
import {getLineNumber, getSideByLineEl} from '../gr-diff/gr-diff-utils';
-import {fireAlert, fireEvent} from '../../../utils/event-util';
+import {fireAlert, fire} from '../../../utils/event-util';
import {assertIsDefined} from '../../../utils/common-util';
const TRAILING_WHITESPACE_PATTERN = /\s+$/;
@@ -201,7 +201,7 @@
const isBinary = !!(this.isImageDiff || this.diff.binary);
- this.fireDiffEvent('render-start');
+ fire(this.diffElement, 'render-start', {});
// TODO: processor.process() returns a cancelable promise already.
// Why wrap another one around it?
this.cancelableRenderPromise = makeCancelable(
@@ -215,7 +215,7 @@
this.builder.renderImageDiff();
}
await this.untilGroupsRendered();
- this.fireDiffEvent('render-content');
+ fire(this.diffElement, 'render-content', {});
})
// Mocha testing does not like uncaught rejections, so we catch
// the cancels which are expected and should not throw errors in
@@ -243,11 +243,6 @@
this.replaceGroup(e.detail.contextGroup, e.detail.groups);
};
- private fireDiffEvent<K extends keyof HTMLElementEventMap>(type: K) {
- assertIsDefined(this.diffElement, 'diff table');
- fireEvent(this.diffElement, type);
- }
-
// visible for testing
setupAnnotationLayers() {
this.rangeLayer = new GrRangedCommentLayer();
@@ -360,12 +355,12 @@
newGroups: readonly GrDiffGroup[]
) {
if (!this.builder) return;
- this.fireDiffEvent('render-start');
+ fire(this.diffElement, 'render-start', {});
this.builder.replaceGroup(contextGroup, newGroups);
this.groups = this.groups.filter(g => g !== contextGroup);
this.groups.push(...newGroups);
this.untilGroupsRendered(newGroups).then(() => {
- this.fireDiffEvent('render-content');
+ fire(this.diffElement, 'render-content', {});
});
}
diff --git a/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-builder-element_test.ts b/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-builder-element_test.ts
index 0f02d71..9cf9bae 100644
--- a/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-builder-element_test.ts
+++ b/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-builder-element_test.ts
@@ -30,7 +30,6 @@
import {KeyLocations} from '../gr-diff-processor/gr-diff-processor';
import {BlameInfo} from '../../../types/common';
import {fixture, html, assert} from '@open-wc/testing';
-import {EventType} from '../../../types/events';
const DEFAULT_PREFS = createDefaultDiffPrefs();
@@ -134,7 +133,7 @@
test('_handlePreferenceError triggers alert and javascript error', () => {
const errorStub = sinon.stub();
- diffTable.addEventListener(EventType.SHOW_ALERT, errorStub);
+ diffTable.addEventListener('show-alert', errorStub);
assert.throws(() => element.handlePreferenceError('tab size'));
assert.equal(
errorStub.lastCall.args[0].detail.message,
diff --git a/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-builder-legacy.ts b/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-builder-legacy.ts
index 5270603..b3f3714 100644
--- a/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-builder-legacy.ts
+++ b/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-builder-legacy.ts
@@ -3,10 +3,7 @@
* Copyright 2016 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
-import {
- MovedLinkClickedEventDetail,
- RenderPreferences,
-} from '../../../api/diff';
+import {RenderPreferences} from '../../../api/diff';
import {fire} from '../../../utils/event-util';
import {GrDiffLine, GrDiffLineType, LineNumber} from '../gr-diff/gr-diff-line';
import {GrDiffGroup, GrDiffGroupType} from '../gr-diff/gr-diff-group';
@@ -430,16 +427,10 @@
anchor.setAttribute('href', `#${line}`);
anchor.addEventListener('click', e => {
e.preventDefault();
- anchor.dispatchEvent(
- new CustomEvent<MovedLinkClickedEventDetail>('moved-link-clicked', {
- detail: {
- lineNum: line,
- side,
- },
- composed: true,
- bubbles: true,
- })
- );
+ fire(anchor, 'moved-link-clicked', {
+ lineNum: line,
+ side,
+ });
});
return anchor;
}
diff --git a/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-section.ts b/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-section.ts
index d40fdda..e5d3d2e 100644
--- a/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-section.ts
+++ b/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-section.ts
@@ -9,7 +9,6 @@
DiffInfo,
DiffLayer,
DiffViewMode,
- MovedLinkClickedEventDetail,
RenderPreferences,
Side,
LineNumber,
@@ -27,6 +26,7 @@
import '../gr-range-header/gr-range-header';
import './gr-diff-row';
import {when} from 'lit/directives/when.js';
+import {fire} from '../../../utils/event-util';
@customElement('gr-diff-section')
export class GrDiffSection extends LitElement {
@@ -235,16 +235,11 @@
side: Side,
line: number
) {
- anchor?.dispatchEvent(
- new CustomEvent<MovedLinkClickedEventDetail>('moved-link-clicked', {
- detail: {
- lineNum: line,
- side,
- },
- composed: true,
- bubbles: true,
- })
- );
+ if (!anchor) return;
+ fire(anchor, 'moved-link-clicked', {
+ lineNum: line,
+ side,
+ });
}
}
diff --git a/polygerrit-ui/app/embed/diff/gr-diff-cursor/gr-diff-cursor.ts b/polygerrit-ui/app/embed/diff/gr-diff-cursor/gr-diff-cursor.ts
index 14bb17e..9e3640b 100644
--- a/polygerrit-ui/app/embed/diff/gr-diff-cursor/gr-diff-cursor.ts
+++ b/polygerrit-ui/app/embed/diff/gr-diff-cursor/gr-diff-cursor.ts
@@ -8,7 +8,8 @@
import {
DiffViewMode,
GrDiffCursor as GrDiffCursorApi,
- LineNumberEventDetail,
+ LineNumber,
+ LineSelectedEventDetail,
} from '../../../api/diff';
import {ScrollMode, Side} from '../../../constants/constants';
import {toggleClass} from '../../../utils/dom-util';
@@ -19,6 +20,7 @@
import {GrDiffLineType} from '../gr-diff/gr-diff-line';
import {GrDiffGroupType} from '../gr-diff/gr-diff-group';
import {GrDiff} from '../gr-diff/gr-diff';
+import {fire} from '../../../utils/event-util';
type GrDiffRowType = GrDiffLineType | GrDiffGroupType;
@@ -237,7 +239,7 @@
}
moveToLineNumber(
- number: number,
+ number: LineNumber,
side: Side,
path?: string,
intentionalMove?: boolean
@@ -352,13 +354,10 @@
this.preventAutoScrollOnManualScroll = false;
};
- private _boundHandleDiffLineSelected = (event: Event) => {
- const customEvent = event as CustomEvent;
- this.moveToLineNumber(
- customEvent.detail.number,
- customEvent.detail.side,
- customEvent.detail.path
- );
+ private _boundHandleDiffLineSelected = (
+ e: CustomEvent<LineSelectedEventDetail>
+ ) => {
+ this.moveToLineNumber(e.detail.number, e.detail.side, e.detail.path);
};
createCommentInPlace() {
@@ -485,16 +484,10 @@
const address = this.getAddressFor(row, side);
if (address) {
const {leftSide, number} = address;
- row.dispatchEvent(
- new CustomEvent<LineNumberEventDetail>(event, {
- detail: {
- lineNum: number,
- side: leftSide ? Side.LEFT : Side.RIGHT,
- },
- composed: true,
- bubbles: true,
- })
- );
+ fire(row, event, {
+ lineNum: number,
+ side: leftSide ? Side.LEFT : Side.RIGHT,
+ });
}
}
@@ -580,7 +573,7 @@
}
_findRowByNumberAndFile(
- targetNumber: number,
+ targetNumber: LineNumber,
side: Side,
path?: string
): HTMLElement | undefined {
diff --git a/polygerrit-ui/app/embed/diff/gr-diff-highlight/gr-diff-highlight.ts b/polygerrit-ui/app/embed/diff/gr-diff-highlight/gr-diff-highlight.ts
index 0714645..02f2233 100644
--- a/polygerrit-ui/app/embed/diff/gr-diff-highlight/gr-diff-highlight.ts
+++ b/polygerrit-ui/app/embed/diff/gr-diff-highlight/gr-diff-highlight.ts
@@ -20,6 +20,7 @@
} from '../gr-diff/gr-diff-utils';
import {debounce, DelayedTask} from '../../../utils/async-util';
import {assertIsDefined, queryAndAssert} from '../../../utils/common-util';
+import {fire} from '../../../utils/event-util';
interface SidedRange {
side: Side;
@@ -458,13 +459,9 @@
}
private fireCreateRangeComment(side: Side, range: CommentRange) {
- this.diffTable?.dispatchEvent(
- new CustomEvent('create-range-comment', {
- detail: {side, range},
- composed: true,
- bubbles: true,
- })
- );
+ if (this.diffTable) {
+ fire(this.diffTable, 'create-range-comment', {side, range});
+ }
this.removeActionBox();
}
diff --git a/polygerrit-ui/app/embed/diff/gr-diff-image-viewer/gr-image-viewer.ts b/polygerrit-ui/app/embed/diff/gr-diff-image-viewer/gr-image-viewer.ts
index 8a92bcc..e8575ff 100644
--- a/polygerrit-ui/app/embed/diff/gr-diff-image-viewer/gr-image-viewer.ts
+++ b/polygerrit-ui/app/embed/diff/gr-diff-image-viewer/gr-image-viewer.ts
@@ -24,14 +24,10 @@
import {classMap} from 'lit/directives/class-map.js';
import {StyleInfo, styleMap} from 'lit/directives/style-map.js';
-import {
- createEvent,
- Dimensions,
- fitToFrame,
- FrameConstrainer,
- Point,
- Rect,
-} from './util';
+import {Dimensions, fitToFrame, FrameConstrainer, Point, Rect} from './util';
+import {ValueChangedEvent} from '../../../types/events';
+import {fire} from '../../../utils/event-util';
+import {ImageDiffAction} from '../../../api/diff';
const DRAG_DEAD_ZONE_PIXELS = 5;
@@ -686,27 +682,25 @@
});
}
+ fireAction(detail: ImageDiffAction) {
+ fire(this, 'image-diff-action', detail);
+ }
+
selectBase() {
if (!this.baseUrl) return;
this.baseSelected = true;
- this.dispatchEvent(
- createEvent({type: 'version-switcher-clicked', button: 'base'})
- );
+ this.fireAction({type: 'version-switcher-clicked', button: 'base'});
}
selectRevision() {
if (!this.revisionUrl) return;
this.baseSelected = false;
- this.dispatchEvent(
- createEvent({type: 'version-switcher-clicked', button: 'revision'})
- );
+ this.fireAction({type: 'version-switcher-clicked', button: 'revision'});
}
manualBlink() {
this.toggleImage();
- this.dispatchEvent(
- createEvent({type: 'version-switcher-clicked', button: 'switch'})
- );
+ this.fireAction({type: 'version-switcher-clicked', button: 'switch'});
}
private toggleImage() {
@@ -717,9 +711,10 @@
toggleAutomaticBlink() {
this.automaticBlink = !this.automaticBlink;
- this.dispatchEvent(
- createEvent({type: 'automatic-blink-changed', value: this.automaticBlink})
- );
+ this.fireAction({
+ type: 'automatic-blink-changed',
+ value: this.automaticBlink,
+ });
}
private updateAutomaticBlink() {
@@ -751,52 +746,42 @@
private toggleHighlight(source: 'controls' | 'magnifier') {
this.showHighlight = !this.showHighlight;
- this.dispatchEvent(
- createEvent({
- type: 'highlight-changes-changed',
- value: this.showHighlight,
- source,
- })
- );
+ this.fireAction({
+ type: 'highlight-changes-changed',
+ value: this.showHighlight,
+ source,
+ });
}
- zoomControlChanged(event: CustomEvent) {
+ zoomControlChanged(event: ValueChangedEvent<'fit' | number>) {
const value = event.detail.value;
if (!value) return;
if (value === 'fit') {
this.scaledSelected = true;
- this.dispatchEvent(
- createEvent({type: 'zoom-level-changed', scale: 'fit'})
- );
+ this.fireAction({type: 'zoom-level-changed', scale: 'fit'});
}
- if (value > 0) {
+ if (typeof value === 'number' && value > 0) {
this.scaledSelected = false;
this.scale = value;
- this.dispatchEvent(
- createEvent({type: 'zoom-level-changed', scale: value})
- );
+ this.fireAction({type: 'zoom-level-changed', scale: value});
}
this.updateSizes();
}
followMouseChanged() {
this.followMouse = !this.followMouse;
- this.dispatchEvent(
- createEvent({type: 'follow-mouse-changed', value: this.followMouse})
- );
+ this.fireAction({type: 'follow-mouse-changed', value: this.followMouse});
}
pickColor(value: string) {
this.checkerboardSelected = false;
this.backgroundColor = value;
- this.dispatchEvent(createEvent({type: 'background-color-changed', value}));
+ this.fireAction({type: 'background-color-changed', value});
}
pickCheckerboard() {
this.checkerboardSelected = true;
- this.dispatchEvent(
- createEvent({type: 'background-color-changed', value: 'checkerboard'})
- );
+ this.fireAction({type: 'background-color-changed', value: 'checkerboard'});
}
mousemoveImageArea(event: MouseEvent) {
@@ -849,9 +834,9 @@
// external mice.
if (distance < DRAG_DEAD_ZONE_PIXELS) {
this.toggleImage();
- this.dispatchEvent(createEvent({type: 'magnifier-clicked'}));
+ this.fireAction({type: 'magnifier-clicked'});
} else {
- this.dispatchEvent(createEvent({type: 'magnifier-dragged'}));
+ this.fireAction({type: 'magnifier-dragged'});
}
}
@@ -894,17 +879,17 @@
if (!this.ownsMouseDown) return;
this.grabbing = false;
this.ownsMouseDown = false;
- this.dispatchEvent(createEvent({type: 'magnifier-dragged'}));
+ this.fireAction({type: 'magnifier-dragged'});
}
dragstartMagnifier(event: DragEvent) {
event.preventDefault();
}
- onOverviewCenterUpdated(event: CustomEvent) {
+ onOverviewCenterUpdated(event: CustomEvent<Point>) {
this.frameConstrainer.requestCenter({
- x: event.detail.x as number,
- y: event.detail.y as number,
+ x: event.detail.x,
+ y: event.detail.y,
});
this.updateFrames();
}
@@ -955,4 +940,7 @@
interface HTMLElementTagNameMap {
'gr-image-viewer': GrImageViewer;
}
+ interface HTMLElementEventMap {
+ 'image-diff-action': CustomEvent<ImageDiffAction>;
+ }
}
diff --git a/polygerrit-ui/app/embed/diff/gr-diff-image-viewer/gr-overview-image.ts b/polygerrit-ui/app/embed/diff/gr-diff-image-viewer/gr-overview-image.ts
index 1bc1447..21a7cf8 100644
--- a/polygerrit-ui/app/embed/diff/gr-diff-image-viewer/gr-overview-image.ts
+++ b/polygerrit-ui/app/embed/diff/gr-diff-image-viewer/gr-overview-image.ts
@@ -7,8 +7,9 @@
import {customElement, property, query, state} from 'lit/decorators.js';
import {StyleInfo, styleMap} from 'lit/directives/style-map.js';
import {ImageDiffAction} from '../../../api/diff';
+import {fire} from '../../../utils/event-util';
-import {createEvent, Dimensions, fitToFrame, Point, Rect} from './util';
+import {Dimensions, fitToFrame, Point, Rect} from './util';
/**
* Displays a scaled-down version of an image with a draggable frame for
@@ -243,7 +244,7 @@
const detail: ImageDiffAction = {
type: this.dragging ? 'overview-frame-dragged' : 'overview-image-clicked',
};
- this.dispatchEvent(createEvent(detail));
+ fire(this, 'image-diff-action', detail);
this.dragging = false;
this.closeOverlay();
@@ -297,13 +298,7 @@
}
private notifyNewCenter(center: Point) {
- this.dispatchEvent(
- new CustomEvent('center-updated', {
- detail: {...center},
- bubbles: true,
- composed: true,
- })
- );
+ fire(this, 'center-updated', {...center});
}
}
@@ -311,4 +306,7 @@
interface HTMLElementTagNameMap {
'gr-overview-image': GrOverviewImage;
}
+ interface HTMLElementEventMap {
+ 'center-updated': CustomEvent<Point>;
+ }
}
diff --git a/polygerrit-ui/app/embed/diff/gr-diff-image-viewer/util.ts b/polygerrit-ui/app/embed/diff/gr-diff-image-viewer/util.ts
index 38a07b7..896dc11 100644
--- a/polygerrit-ui/app/embed/diff/gr-diff-image-viewer/util.ts
+++ b/polygerrit-ui/app/embed/diff/gr-diff-image-viewer/util.ts
@@ -3,7 +3,6 @@
* Copyright 2021 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
-import {ImageDiffAction} from '../../../api/diff';
export interface Point {
x: number;
@@ -224,13 +223,3 @@
};
}
}
-
-export function createEvent(
- detail: ImageDiffAction
-): CustomEvent<ImageDiffAction> {
- return new CustomEvent('image-diff-action', {
- detail,
- bubbles: true,
- composed: true,
- });
-}
diff --git a/polygerrit-ui/app/embed/diff/gr-diff/gr-diff.ts b/polygerrit-ui/app/embed/diff/gr-diff/gr-diff.ts
index b9a01ce..34e9d5a 100644
--- a/polygerrit-ui/app/embed/diff/gr-diff/gr-diff.ts
+++ b/polygerrit-ui/app/embed/diff/gr-diff/gr-diff.ts
@@ -29,7 +29,10 @@
} from './gr-diff-utils';
import {BlameInfo, CommentRange, ImageInfo} from '../../../types/common';
import {DiffInfo, DiffPreferencesInfo} from '../../../types/diff';
-import {GrDiffHighlight} from '../gr-diff-highlight/gr-diff-highlight';
+import {
+ CreateRangeCommentEventDetail,
+ GrDiffHighlight,
+} from '../gr-diff-highlight/gr-diff-highlight';
import {
GrDiffBuilderElement,
getLineNumberCellWidth,
@@ -42,7 +45,7 @@
Side,
} from '../../../constants/constants';
import {KeyLocations} from '../gr-diff-processor/gr-diff-processor';
-import {fire, fireAlert, fireEvent} from '../../../utils/event-util';
+import {fire, fireAlert} from '../../../utils/event-util';
import {MovedLinkClickedEvent, ValueChangedEvent} from '../../../types/events';
import {getContentEditableRange} from '../../../utils/safari-selection-util';
import {AbortStop} from '../../../api/core';
@@ -989,8 +992,10 @@
constructor() {
super();
provide(this, diffModelToken, () => this.diffModel);
- this.addEventListener('create-range-comment', (e: Event) =>
- this.handleCreateRangeComment(e as CustomEvent)
+ this.addEventListener(
+ 'create-range-comment',
+ (e: CustomEvent<CreateRangeCommentEventDetail>) =>
+ this.handleCreateRangeComment(e)
);
this.addEventListener('render-content', () => this.handleRenderContent());
this.addEventListener('moved-link-clicked', (e: MovedLinkClickedEvent) => {
@@ -1268,10 +1273,10 @@
threadEl: GrDiffThreadElement
) {
hoverEl.addEventListener('mouseenter', () => {
- fireEvent(threadEl, 'comment-thread-mouseenter');
+ fire(threadEl, 'comment-thread-mouseenter', {});
});
hoverEl.addEventListener('mouseleave', () => {
- fireEvent(threadEl, 'comment-thread-mouseleave');
+ fire(threadEl, 'comment-thread-mouseleave', {});
});
}
@@ -1343,17 +1348,11 @@
}
private dispatchSelectedLine(number: LineNumber, side: Side) {
- this.dispatchEvent(
- new CustomEvent('line-selected', {
- detail: {
- number,
- side,
- path: this.path,
- },
- composed: true,
- bubbles: true,
- })
- );
+ fire(this, 'line-selected', {
+ number,
+ side,
+ path: this.path,
+ });
}
addDraftAtLine(el: Element) {
@@ -1386,7 +1385,9 @@
}
}
- private handleCreateRangeComment(e: CustomEvent) {
+ private handleCreateRangeComment(
+ e: CustomEvent<CreateRangeCommentEventDetail>
+ ) {
const range = e.detail.range;
const side = e.detail.side;
this.createCommentForSelection(side, range);
@@ -1403,18 +1404,12 @@
if (!contentEl) throw new Error('content el not found for line el');
side = side ?? this.getCommentSideByLineAndContent(lineEl, contentEl);
assertIsDefined(this.path, 'path');
- this.dispatchEvent(
- new CustomEvent<CreateCommentEventDetail>('create-comment', {
- bubbles: true,
- composed: true,
- detail: {
- path: this.path,
- side,
- lineNum,
- range,
- },
- })
- );
+ fire(this, 'create-comment', {
+ path: this.path,
+ side,
+ lineNum,
+ range,
+ });
}
private getCommentSideByLineAndContent(
@@ -1566,10 +1561,10 @@
// (client), although it was not actually rendered. Clients need to know
// when it is safe to perform operations like cursor moves, for example,
// and if changing an input actually requires a reload of the diff table.
- // Since `fireEvent` is synchronous it allows clients to be aware when an
+ // Since `fire` is synchronous it allows clients to be aware when an
// async render is needed and that they can wait for a further `render`
// event to actually take further action.
- fireEvent(this, 'render-required');
+ fire(this, 'render-required', {});
this.renderDiffTableTask = debounceP(
this.renderDiffTableTask,
async () => await this.renderDiffTable()
@@ -1584,7 +1579,7 @@
async renderDiffTable() {
this.unobserveNodes();
if (!this.diff || !this.prefs) {
- fireEvent(this, 'render');
+ fire(this, 'render', {});
return;
}
if (
@@ -1594,7 +1589,7 @@
this.safetyBypass === null
) {
this.showWarning = true;
- fireEvent(this, 'render');
+ fire(this, 'render', {});
return;
}
@@ -1638,7 +1633,7 @@
this.observeNodes();
// We are just converting 'render-content' into 'render' here. Maybe we
// should retire the 'render' event in favor of 'render-content'?
- fireEvent(this, 'render');
+ fire(this, 'render', {});
}
private observeNodes() {
@@ -1831,6 +1826,9 @@
'gr-diff': GrDiff;
}
interface HTMLElementEventMap {
+ 'comment-thread-mouseenter': CustomEvent<{}>;
+ 'comment-thread-mouseleave': CustomEvent<{}>;
'loading-changed': ValueChangedEvent<boolean>;
+ 'render-required': CustomEvent<{}>;
}
}
diff --git a/polygerrit-ui/app/embed/diff/gr-selection-action-box/gr-selection-action-box.ts b/polygerrit-ui/app/embed/diff/gr-selection-action-box/gr-selection-action-box.ts
index cb08e55..68aa3b4 100644
--- a/polygerrit-ui/app/embed/diff/gr-selection-action-box/gr-selection-action-box.ts
+++ b/polygerrit-ui/app/embed/diff/gr-selection-action-box/gr-selection-action-box.ts
@@ -5,7 +5,7 @@
*/
import '../../../elements/shared/gr-tooltip/gr-tooltip';
import {GrTooltip} from '../../../elements/shared/gr-tooltip/gr-tooltip';
-import {fireEvent} from '../../../utils/event-util';
+import {fire} from '../../../utils/event-util';
import {css, html, LitElement} from 'lit';
import {customElement, property, query, state} from 'lit/decorators.js';
import {sharedStyles} from '../../../styles/shared-styles';
@@ -14,16 +14,14 @@
interface HTMLElementTagNameMap {
'gr-selection-action-box': GrSelectionActionBox;
}
+ interface HTMLElementEventMap {
+ /** Fired when the comment creation action was taken (click). */
+ 'create-comment-requested': CustomEvent<{}>;
+ }
}
@customElement('gr-selection-action-box')
export class GrSelectionActionBox extends LitElement {
- /**
- * Fired when the comment creation action was taken (click).
- *
- * @event create-comment-requested
- */
-
@query('#tooltip')
tooltip?: GrTooltip;
@@ -133,6 +131,6 @@
} // 0 = main button
e.preventDefault();
e.stopPropagation();
- fireEvent(this, 'create-comment-requested');
+ fire(this, 'create-comment-requested', {});
}
}
diff --git a/polygerrit-ui/app/mixins/hovercard-mixin/hovercard-mixin.ts b/polygerrit-ui/app/mixins/hovercard-mixin/hovercard-mixin.ts
index b383fd7..4f79e4c 100644
--- a/polygerrit-ui/app/mixins/hovercard-mixin/hovercard-mixin.ts
+++ b/polygerrit-ui/app/mixins/hovercard-mixin/hovercard-mixin.ts
@@ -6,7 +6,7 @@
import {Constructor} from '../../utils/common-util';
import {LitElement, PropertyValues} from 'lit';
import {property, query} from 'lit/decorators.js';
-import {EventType, ShowAlertEventDetail} from '../../types/events';
+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';
@@ -303,7 +303,7 @@
dispatchEventThroughTarget(eventName: string): void;
dispatchEventThroughTarget(
- eventName: EventType.SHOW_ALERT,
+ eventName: 'show-alert',
detail: ShowAlertEventDetail
): void;
diff --git a/polygerrit-ui/app/models/change/change-model.ts b/polygerrit-ui/app/models/change/change-model.ts
index 446822f..ad5b217 100644
--- a/polygerrit-ui/app/models/change/change-model.ts
+++ b/polygerrit-ui/app/models/change/change-model.ts
@@ -26,6 +26,7 @@
import {
computeAllPatchSets,
computeLatestPatchNum,
+ computeLatestPatchNumWithEdit,
} from '../../utils/patch-set-util';
import {ParsedChangeInfo} from '../../types/types';
import {fireAlert} from '../../utils/event-util';
@@ -191,6 +192,10 @@
computeLatestPatchNum(patchsets)
);
+ public readonly latestPatchNumWithEdit$ = select(this.patchsets$, patchsets =>
+ computeLatestPatchNumWithEdit(patchsets)
+ );
+
/**
* Emits the current patchset number. If the route does not define the current
* patchset num, then this selector waits for the change to be defined and
@@ -203,7 +208,7 @@
combineLatest([
this.viewModel.state$,
this.state$,
- this.latestPatchNum$,
+ this.latestPatchNumWithEdit$,
]).pipe(
/**
* If you depend on both, view model and change state, then you want to
diff --git a/polygerrit-ui/app/models/checks/checks-model.ts b/polygerrit-ui/app/models/checks/checks-model.ts
index 4715abf..2aa4aa4 100644
--- a/polygerrit-ui/app/models/checks/checks-model.ts
+++ b/polygerrit-ui/app/models/checks/checks-model.ts
@@ -51,7 +51,7 @@
import {getShaByPatchNum} from '../../utils/patch-set-util';
import {ReportingService} from '../../services/gr-reporting/gr-reporting';
import {Execution, Interaction, Timing} from '../../constants/reporting';
-import {fireAlert, fireEvent} from '../../utils/event-util';
+import {fireAlert, fire} from '../../utils/event-util';
import {Model} from '../model';
import {define} from '../dependency';
import {
@@ -716,7 +716,7 @@
if (result.errorMessage || result.message) {
fireAlert(document, `${result.message ?? result.errorMessage}`);
} else {
- fireEvent(document, 'hide-alert');
+ fire(document, 'hide-alert', {});
}
if (result.shouldReload) {
this.reloadForCheck(run?.checkName);
diff --git a/polygerrit-ui/app/models/comments/comments-model.ts b/polygerrit-ui/app/models/comments/comments-model.ts
index 1fdf342..0e78274 100644
--- a/polygerrit-ui/app/models/comments/comments-model.ts
+++ b/polygerrit-ui/app/models/comments/comments-model.ts
@@ -31,7 +31,7 @@
import {select} from '../../utils/observable-util';
import {define} from '../dependency';
import {combineLatest, forkJoin, from, Observable, of} from 'rxjs';
-import {fire, fireAlert, fireEvent} from '../../utils/event-util';
+import {fire, fireAlert} from '../../utils/event-util';
import {CURRENT} from '../../utils/patch-set-util';
import {RestApiService} from '../../services/gr-rest-api/gr-rest-api';
import {ChangeModel} from '../change/change-model';
@@ -43,7 +43,6 @@
import {Model} from '../model';
import {Deduping} from '../../api/reporting';
import {extractMentionedUsers, getUserId} from '../../utils/account-util';
-import {EventType} from '../../types/events';
import {SpecialFilePath} from '../../constants/constants';
import {AccountsModel} from '../accounts-model/accounts-model';
import {
@@ -643,7 +642,7 @@
this.modifyState(s => deleteDraft(s, draft));
// We don't store empty discarded drafts and don't need an UNDO then.
if (draft.message?.trim()) {
- fire(document, EventType.SHOW_ALERT, {
+ fire(document, 'show-alert', {
message: 'Draft Discarded',
action: 'Undo',
callback: () => this.restoreDraft(draft.id),
@@ -693,7 +692,7 @@
private updateRequestToast(requestFailed?: boolean) {
if (this.numPendingDraftRequests === 0 && !requestFailed) {
- fireEvent(document, 'hide-alert');
+ fire(document, 'hide-alert', {});
return;
}
const message = getSavingMessage(
diff --git a/polygerrit-ui/app/models/views/change.ts b/polygerrit-ui/app/models/views/change.ts
index 153777f..d2a0dd8 100644
--- a/polygerrit-ui/app/models/views/change.ts
+++ b/polygerrit-ui/app/models/views/change.ts
@@ -79,6 +79,7 @@
/** These properties apply to the DIFF child view only. */
diffView?: {
path?: string;
+ // TODO: Use LineNumber as a type, i.e. accept FILE and LOST.
lineNum?: number;
leftSide?: boolean;
};
diff --git a/polygerrit-ui/app/services/flags/flags.ts b/polygerrit-ui/app/services/flags/flags.ts
index 29e9259..7488e79 100644
--- a/polygerrit-ui/app/services/flags/flags.ts
+++ b/polygerrit-ui/app/services/flags/flags.ts
@@ -19,5 +19,4 @@
PUSH_NOTIFICATIONS_DEVELOPER = 'UiFeature__push_notifications_developer',
PUSH_NOTIFICATIONS = 'UiFeature__push_notifications',
SUGGEST_EDIT = 'UiFeature__suggest_edit',
- REBASE_CHAIN = 'UiFeature__rebase_chain',
}
diff --git a/polygerrit-ui/app/services/gr-auth/gr-auth_mock.ts b/polygerrit-ui/app/services/gr-auth/gr-auth_mock.ts
index 480484e..37c2311 100644
--- a/polygerrit-ui/app/services/gr-auth/gr-auth_mock.ts
+++ b/polygerrit-ui/app/services/gr-auth/gr-auth_mock.ts
@@ -3,6 +3,7 @@
* Copyright 2020 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
+import {fire} from '../../utils/event-util';
import {
AuthRequestInit,
AuthService,
@@ -28,14 +29,10 @@
private _setStatus(status: AuthStatus) {
if (this._status === status) return;
if (this._status === AuthStatus.AUTHED) {
- document.dispatchEvent(
- new CustomEvent('auth-error', {
- detail: {
- message: Auth.CREDS_EXPIRED_MSG,
- action: 'Refresh credentials',
- },
- })
- );
+ fire(document, 'auth-error', {
+ message: Auth.CREDS_EXPIRED_MSG,
+ action: 'Refresh credentials',
+ });
}
this._status = status;
}
diff --git a/polygerrit-ui/app/styles/themes/app-theme.ts b/polygerrit-ui/app/styles/themes/app-theme.ts
index 107ee16..0503e4c 100644
--- a/polygerrit-ui/app/styles/themes/app-theme.ts
+++ b/polygerrit-ui/app/styles/themes/app-theme.ts
@@ -278,6 +278,11 @@
--robot-comment-background-color: var(--blue-50);
--unresolved-comment-background-color: #fef7e0;
+
+ /* Suggest edits */
+ --user-suggestion-header-background: var(--gray-700);
+ --user-suggestion-header-color: white;
+
/* vote background colors */
--vote-color-approved: var(--green-300);
--vote-color-disliked: var(--red-50);
diff --git a/polygerrit-ui/app/styles/themes/dark-theme.ts b/polygerrit-ui/app/styles/themes/dark-theme.ts
index a183c86..dc3d4e9 100644
--- a/polygerrit-ui/app/styles/themes/dark-theme.ts
+++ b/polygerrit-ui/app/styles/themes/dark-theme.ts
@@ -138,6 +138,10 @@
--robot-comment-background-color: #1e3a5f;
--unresolved-comment-background-color: #614a19;
+ /* Suggest edits */
+ --user-suggestion-header-background: var(--gray-700);
+ --user-suggestion-header-color: white;
+
/* vote background colors */
--vote-color-approved: var(--green-300);
--vote-color-disliked: var(--red-tonal);
diff --git a/polygerrit-ui/app/types/events.ts b/polygerrit-ui/app/types/events.ts
index e2612b0..8b02fdf 100644
--- a/polygerrit-ui/app/types/events.ts
+++ b/polygerrit-ui/app/types/events.ts
@@ -3,59 +3,44 @@
* Copyright 2020 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
-import {FixSuggestionInfo, PatchSetNum} from './common';
+import {AccountInfo, FixSuggestionInfo, PatchSetNum} from './common';
import {ChangeMessage} from '../utils/comment-util';
import {FetchRequest} from './types';
import {LineNumberEventDetail, MovedLinkClickedEventDetail} from '../api/diff';
import {Category, RunStatus} from '../api/checks';
+import {DropdownLink} from '../elements/shared/gr-dropdown/gr-dropdown';
+import {AutocompleteCommitEvent} from '../elements/shared/gr-autocomplete/gr-autocomplete';
-export enum EventType {
- BIND_VALUE_CHANGED = 'bind-value-changed',
- CHANGE = 'change',
- CHANGED = 'changed',
- CHANGE_MESSAGE_DELETED = 'change-message-deleted',
- COMMIT = 'commit',
- DIALOG_CHANGE = 'dialog-change',
- DROP = 'drop',
- EDITABLE_CONTENT_SAVE = 'editable-content-save',
- GR_RPC_LOG = 'gr-rpc-log',
- IRON_ANNOUNCE = 'iron-announce',
- KEYDOWN = 'keydown',
- KEYPRESS = 'keypress',
- LOCATION_CHANGE = 'location-change',
- MOVED_LINK_CLICKED = 'moved-link-clicked',
- NETWORK_ERROR = 'network-error',
- OPEN_FIX_PREVIEW = 'open-fix-preview',
- PAGE_ERROR = 'page-error',
- RELOAD = 'reload',
- REPLY = 'reply',
- SERVER_ERROR = 'server-error',
- SHORTCUT_TRIGGERERD = 'shortcut-triggered',
- SHOW_ALERT = 'show-alert',
- SHOW_ERROR = 'show-error',
- SHOW_TAB = 'show-tab',
- SHOW_SECONDARY_TAB = 'show-secondary-tab',
- TAP_ITEM = 'tap-item',
- TITLE_CHANGE = 'title-change',
-}
-
+// TODO: Local events that are only fired by one component should also be
+// declared and documented in that component. Don't collect ALL the events here.
+// 'show-alert' for example is fine to keep, because it is fired all over the
+// place. But 'line-cursor-moved-in' is only fired by <gr-diff-cursor>, so let's
+// move it there.
declare global {
interface HTMLElementEventMap {
- /* prettier-ignore */
+ 'add-reviewer': AddReviewerEvent;
'bind-value-changed': BindValueChangeEvent;
- /* prettier-ignore */
+ /** Fired when a 'cancel' button in a dialog was pressed. */
+ // prettier-ignore
+ 'cancel': CustomEvent<{}>;
+ // prettier-ignore
'change': ChangeEvent;
- /* prettier-ignore */
+ // prettier-ignore
'changed': ChangedEvent;
- 'change-message-deleted': ChangeMessageDeletedEvent;
- /* prettier-ignore */
- 'commit': CommitEvent;
+ // prettier-ignore
+ 'close': CustomEvent<{}>;
+ // prettier-ignore
+ 'commit': AutocompleteCommitEvent;
+ /** Fired when a 'confirm' button in a dialog was pressed. */
+ // prettier-ignore
+ 'confirm': CustomEvent<{}>;
'dialog-change': DialogChangeEvent;
- /* prettier-ignore */
+ // prettier-ignore
'drop': DropEvent;
- 'editable-content-save': EditableContentSaveEvent;
+ 'hide-alert': CustomEvent<{}>;
'location-change': LocationChangeEvent;
'iron-announce': IronAnnounceEvent;
+ 'iron-resize': CustomEvent<{}>;
'line-mouse-enter': LineNumberEvent;
'line-mouse-leave': LineNumberEvent;
'line-cursor-moved-in': LineNumberEvent;
@@ -63,10 +48,9 @@
'moved-link-clicked': MovedLinkClickedEvent;
'open-fix-preview': OpenFixPreviewEvent;
'reply-to-comment': ReplyToCommentEvent;
- /* prettier-ignore */
+ // prettier-ignore
'reload': ReloadEvent;
- /* prettier-ignore */
- 'reply': ReplyEvent;
+ 'remove-reviewer': RemoveReviewerEvent;
'show-alert': ShowAlertEvent;
'show-error': ShowErrorEvent;
'show-tab': SwitchTabEvent;
@@ -81,7 +65,7 @@
'gr-rpc-log': RpcLogEvent;
'network-error': NetworkErrorEvent;
'page-error': PageErrorEvent;
- /* prettier-ignore */
+ // prettier-ignore
'reload': ReloadEvent;
'server-error': ServerErrorEvent;
'show-alert': ShowAlertEvent;
@@ -90,6 +74,21 @@
}
}
+export interface AddAccountEventDetail {
+ value: string;
+}
+export type AddAccountEvent = CustomEvent<AddAccountEventDetail>;
+
+export interface AddReviewerEventDetail {
+ reviewer: AccountInfo;
+}
+export type AddReviewerEvent = CustomEvent<AddReviewerEventDetail>;
+
+export interface RemoveReviewerEventDetail {
+ reviewer: AccountInfo;
+}
+export type RemoveReviewerEvent = CustomEvent<RemoveReviewerEventDetail>;
+
export interface BindValueChangeEventDetail {
value: string | undefined;
}
@@ -97,7 +96,8 @@
export type ChangeEvent = InputEvent;
-export type ChangedEvent = CustomEvent<string>;
+// TODO: This event seems to be unused (no listener). Remove?
+export type ChangedEvent = CustomEvent<string | undefined>;
export interface ChangeMessageDeletedEventDetail {
message: ChangeMessage;
@@ -105,8 +105,6 @@
export type ChangeMessageDeletedEvent =
CustomEvent<ChangeMessageDeletedEventDetail>;
-export type CommitEvent = CustomEvent;
-
// TODO(milutin) - remove once new gr-dialog will do it out of the box
// This informs gr-app-element to remove footer, header from a11y tree
export interface DialogChangeEventDetail {
@@ -123,6 +121,13 @@
export type EditableContentSaveEvent =
CustomEvent<EditableContentSaveEventDetail>;
+export interface FileActionTapEventDetail {
+ path: string;
+ action: string;
+}
+
+export type FileActionTapEvent = CustomEvent<FileActionTapEventDetail>;
+
export interface RpcLogEventDetail {
status: number | null;
method: string;
@@ -163,6 +168,7 @@
userWantsToEdit: boolean;
unresolved: boolean;
}
+
export type ReplyToCommentEvent = CustomEvent<ReplyToCommentEventDetail>;
export interface PageErrorEventDetail {
@@ -175,6 +181,11 @@
}
export type ReloadEvent = CustomEvent<ReloadEventDetail>;
+export interface RemoveAccountEventDetail {
+ account: AccountInfo;
+}
+export type RemoveAccountEvent = CustomEvent<RemoveAccountEventDetail>;
+
export interface ReplyEventDetail {
message: ChangeMessage;
}
@@ -200,6 +211,14 @@
}
export type ShowErrorEvent = CustomEvent<ShowErrorEventDetail>;
+export interface ShowReplyDialogEventDetail {
+ value: {
+ reviewersOnly: boolean;
+ ccsOnly: boolean;
+ };
+}
+export type ShowReplyDialogEvent = CustomEvent<ShowReplyDialogEventDetail>;
+
export interface AuthErrorEventDetail {
message: string;
action: string;
@@ -231,7 +250,7 @@
}
export type SwitchTabEvent = CustomEvent<SwitchTabEventDetail>;
-export type TapItemEvent = CustomEvent;
+export type TapItemEvent = CustomEvent<DropdownLink>;
export interface TitleChangeEventDetail {
title: string;
diff --git a/polygerrit-ui/app/utils/async-util.ts b/polygerrit-ui/app/utils/async-util.ts
index 752de62..6ac3211 100644
--- a/polygerrit-ui/app/utils/async-util.ts
+++ b/polygerrit-ui/app/utils/async-util.ts
@@ -62,6 +62,8 @@
/**
* Promise that is resolved after the callback is run or the task is
* cancelled.
+ *
+ * If callback returns a Promise this resolves after the promise is settled.
*/
public readonly promise: Promise<ResolvedDelayedTaskStatus>;
@@ -69,14 +71,28 @@
value: ResolvedDelayedTaskStatus | PromiseLike<ResolvedDelayedTaskStatus>
) => void;
- constructor(private callback: () => void, waitMs = 0) {
+ private callCallbackAndResolveOnCompletion() {
+ let callbackResult;
+ if (this.callback) callbackResult = this.callback();
+ if (callbackResult instanceof Promise) {
+ callbackResult.finally(() => {
+ this.resolvePromise!(ResolvedDelayedTaskStatus.CALLBACK_EXECUTED);
+ });
+ } else {
+ this.resolvePromise!(ResolvedDelayedTaskStatus.CALLBACK_EXECUTED);
+ }
+ }
+
+ constructor(
+ private readonly callback: () => void | Promise<void>,
+ waitMs = 0
+ ) {
this.promise = new Promise(resolve => {
this.resolvePromise = resolve;
this.timerId = window.setTimeout(() => {
if (this.timerId) _testOnly_allTasks.delete(this.timerId);
this.timerId = undefined;
- if (this.callback) this.callback();
- resolve(ResolvedDelayedTaskStatus.CALLBACK_EXECUTED);
+ this.callCallbackAndResolveOnCompletion();
}, waitMs);
_testOnly_allTasks.set(this.timerId, this);
});
@@ -98,8 +114,7 @@
flush() {
if (this.isActive()) {
this.cancelTimer();
- if (this.callback) this.callback();
- this.resolvePromise?.(ResolvedDelayedTaskStatus.CALLBACK_EXECUTED);
+ this.callCallbackAndResolveOnCompletion();
}
}
diff --git a/polygerrit-ui/app/utils/async-util_test.ts b/polygerrit-ui/app/utils/async-util_test.ts
index 9f029b8..ee4f73a 100644
--- a/polygerrit-ui/app/utils/async-util_test.ts
+++ b/polygerrit-ui/app/utils/async-util_test.ts
@@ -6,8 +6,8 @@
import {assert} from '@open-wc/testing';
import {SinonFakeTimers} from 'sinon';
import '../test/common-test-setup';
-import {waitEventLoop} from '../test/test-utils';
-import {asyncForeach, debounceP} from './async-util';
+import {mockPromise, waitEventLoop, waitUntil} from '../test/test-utils';
+import {asyncForeach, debounceP, DelayedTask} from './async-util';
suite('async-util tests', () => {
suite('asyncForeach', () => {
@@ -205,4 +205,16 @@
await waitEventLoop();
});
});
+
+ test('DelayedTask promise resolved when callback is done', async () => {
+ const callbackPromise = mockPromise<void>();
+ const task = new DelayedTask(() => callbackPromise);
+ let completed = false;
+ task.promise.then(() => (completed = true));
+ await waitUntil(() => !task.isActive());
+
+ assert.isFalse(completed);
+ callbackPromise.resolve();
+ await waitUntil(() => completed);
+ });
});
diff --git a/polygerrit-ui/app/utils/comment-util.ts b/polygerrit-ui/app/utils/comment-util.ts
index a92f0f8..34a90de 100644
--- a/polygerrit-ui/app/utils/comment-util.ts
+++ b/polygerrit-ui/app/utils/comment-util.ts
@@ -523,7 +523,8 @@
};
}
-export const USER_SUGGESTION_START_PATTERN = '```suggestion\n';
+export const USER_SUGGESTION_INFO_STRING = 'suggestion';
+export const USER_SUGGESTION_START_PATTERN = `\`\`\`${USER_SUGGESTION_INFO_STRING}\n`;
// This can either mean a user or a checks provided fix.
// "Provided" means that the fix is sent along with the request
diff --git a/polygerrit-ui/app/utils/event-util.ts b/polygerrit-ui/app/utils/event-util.ts
index 49d5382..3704557 100644
--- a/polygerrit-ui/app/utils/event-util.ts
+++ b/polygerrit-ui/app/utils/event-util.ts
@@ -6,47 +6,34 @@
import {FetchRequest} from '../types/types';
import {
DialogChangeEventDetail,
- EventType,
SwitchTabEventDetail,
TabState,
} from '../types/events';
-export function fireEvent(target: EventTarget, type: string) {
- target.dispatchEvent(
- new CustomEvent(type, {
- composed: true,
- bubbles: true,
- })
- );
-}
-
export type HTMLElementEventDetailType<K extends keyof HTMLElementEventMap> =
- HTMLElementEventMap[K] extends CustomEvent<infer DT>
- ? unknown extends DT
- ? never
- : DT
- : never;
+ HTMLElementEventMap[K] extends CustomEvent<infer DT> ? DT : never;
type DocumentEventDetailType<K extends keyof DocumentEventMap> =
- DocumentEventMap[K] extends CustomEvent<infer DT>
- ? unknown extends DT
- ? never
- : DT
- : never;
+ DocumentEventMap[K] extends CustomEvent<infer DT> ? DT : never;
export function fire<K extends keyof DocumentEventMap>(
- target: Document,
+ target: Document | undefined,
type: K,
detail: DocumentEventDetailType<K>
): void;
export function fire<K extends keyof HTMLElementEventMap>(
- target: EventTarget,
+ target: EventTarget | undefined,
type: K,
detail: HTMLElementEventDetailType<K>
): void;
-export function fire<T>(target: EventTarget, type: string, detail: T) {
+export function fire<T>(
+ target: EventTarget | undefined,
+ type: string,
+ detail: T
+) {
+ if (!target) return;
target.dispatchEvent(
new CustomEvent<T>(type, {
detail,
@@ -56,28 +43,60 @@
);
}
+export function fireNoBubble<K extends keyof HTMLElementEventMap, T>(
+ target: EventTarget,
+ type: K,
+ detail: T
+) {
+ target.dispatchEvent(
+ new CustomEvent<T>(type, {
+ detail,
+ composed: true,
+ bubbles: false,
+ })
+ );
+}
+
+export function fireNoBubbleNoCompose<K extends keyof HTMLElementEventMap, T>(
+ target: EventTarget,
+ type: K,
+ detail: T
+) {
+ target.dispatchEvent(
+ new CustomEvent<T>(type, {
+ detail,
+ composed: false,
+ bubbles: false,
+ })
+ );
+}
+
export function fireAlert(target: EventTarget, message: string) {
- fire(target, EventType.SHOW_ALERT, {message, showDismiss: true});
+ fire(target, 'show-alert', {message, showDismiss: true});
+}
+
+export function fireError(target: EventTarget, message: string) {
+ fire(target, 'show-error', {message});
}
export function firePageError(response?: Response | null) {
if (response === null) response = undefined;
- fire(document, EventType.PAGE_ERROR, {response});
+ fire(document, 'page-error', {response});
}
export function fireServerError(response: Response, request?: FetchRequest) {
- fire(document, EventType.SERVER_ERROR, {
+ fire(document, 'server-error', {
response,
request,
});
}
export function fireNetworkError(error: Error) {
- fire(document, EventType.NETWORK_ERROR, {error});
+ fire(document, 'network-error', {error});
}
export function fireTitleChange(target: EventTarget, title: string) {
- fire(target, EventType.TITLE_CHANGE, {title});
+ fire(target, 'title-change', {title});
}
// TODO(milutin) - remove once new gr-dialog will do it out of the box
@@ -86,11 +105,11 @@
target: EventTarget,
detail: DialogChangeEventDetail
) {
- fire(target, EventType.DIALOG_CHANGE, detail);
+ fire(target, 'dialog-change', detail);
}
export function fireIronAnnounce(target: EventTarget, text: string) {
- fire(target, EventType.IRON_ANNOUNCE, {text});
+ fire(target, 'iron-announce', {text});
}
export function fireShowTab(
@@ -100,11 +119,11 @@
tabState?: TabState
) {
const detail: SwitchTabEventDetail = {tab, scrollIntoView, tabState};
- fire(target, EventType.SHOW_TAB, detail);
+ fire(target, 'show-tab', detail);
}
export function fireReload(target: EventTarget, clearPatchset?: boolean) {
- fire(target, EventType.RELOAD, {clearPatchset: !!clearPatchset});
+ fire(target, 'reload', {clearPatchset: !!clearPatchset});
}
export function waitForEventOnce<K extends keyof HTMLElementEventMap>(
diff --git a/polygerrit-ui/app/utils/label-util.ts b/polygerrit-ui/app/utils/label-util.ts
index aaa35a4..8929e9c 100644
--- a/polygerrit-ui/app/utils/label-util.ts
+++ b/polygerrit-ui/app/utils/label-util.ts
@@ -153,7 +153,14 @@
return false;
}
-export function canVote(label: DetailedLabelInfo, account: AccountInfo) {
+// This method is checking labels.all from change detail,
+// that shows only permitted voting for reviewers or CC.
+// It doesn't have permitted votes for owner. You
+// can see permitted labels for logged in user in change.permitted_labels
+export function canReviewerVote(
+ label: DetailedLabelInfo,
+ account: AccountInfo
+) {
const approvalInfo = getApprovalInfo(label, account);
if (!approvalInfo) return false;
if (approvalInfo.permitted_voting_range) {
diff --git a/polygerrit-ui/app/utils/patch-set-util.ts b/polygerrit-ui/app/utils/patch-set-util.ts
index 183671f..7e16ad9 100644
--- a/polygerrit-ui/app/utils/patch-set-util.ts
+++ b/polygerrit-ui/app/utils/patch-set-util.ts
@@ -265,6 +265,16 @@
return latest;
}
+// Basically is computeLatestPatchNum but allows "edits".
+export function computeLatestPatchNumWithEdit(
+ allPatchSets?: PatchSet[]
+): RevisionPatchSetNum | undefined {
+ if (!allPatchSets || !allPatchSets.length) {
+ return undefined;
+ }
+ return allPatchSets[0].num;
+}
+
export function computePredecessor(
patchset?: PatchSetNum
): BasePatchSetNum | undefined {