Merge changes I343425ab,Ieaa18566
* changes:
Do not allow creations of comments with invalid inReplyTo
Add robot comments creation and retrieval to the test api
diff --git a/Documentation/rest-api-changes.txt b/Documentation/rest-api-changes.txt
index fbad212..4227655 100644
--- a/Documentation/rest-api-changes.txt
+++ b/Documentation/rest-api-changes.txt
@@ -7393,6 +7393,10 @@
endpoint is the topic of the change being reverted, and the default for the
RevertSubmission endpoint is `revert-{submission_id}-{timestamp.now}`.
Topic can't contain quotation marks.
+|`work_in_progress` |optional|
+When present, change is marked as Work In Progress. This will also override
+the notify value to `OWNER`. +
+If not set, the default is false.
|=============================
[[revert-submission-info]]
diff --git a/java/com/google/gerrit/acceptance/AbstractDaemonTest.java b/java/com/google/gerrit/acceptance/AbstractDaemonTest.java
index 357ea0c..78a621c 100644
--- a/java/com/google/gerrit/acceptance/AbstractDaemonTest.java
+++ b/java/com/google/gerrit/acceptance/AbstractDaemonTest.java
@@ -429,13 +429,16 @@
baseConfig.setInt("receive", null, "changeUpdateThreads", 4);
Module module = createModule();
+ Module auditModule = createAuditModule();
if (classDesc.equals(methodDesc) && !classDesc.sandboxed() && !methodDesc.sandboxed()) {
if (commonServer == null) {
- commonServer = GerritServer.initAndStart(temporaryFolder, classDesc, baseConfig, module);
+ commonServer =
+ GerritServer.initAndStart(temporaryFolder, classDesc, baseConfig, module, auditModule);
}
server = commonServer;
} else {
- server = GerritServer.initAndStart(temporaryFolder, methodDesc, baseConfig, module);
+ server =
+ GerritServer.initAndStart(temporaryFolder, methodDesc, baseConfig, module, auditModule);
}
server.getTestInjector().injectMembers(this);
@@ -528,6 +531,11 @@
return null;
}
+ /** Override to bind an alternative audit Guice module */
+ public Module createAuditModule() {
+ return null;
+ }
+
protected void initSsh() throws Exception {
if (testRequiresSsh
&& SshMode.useSsh()
diff --git a/java/com/google/gerrit/acceptance/GerritServer.java b/java/com/google/gerrit/acceptance/GerritServer.java
index 030c402..5942c0f 100644
--- a/java/com/google/gerrit/acceptance/GerritServer.java
+++ b/java/com/google/gerrit/acceptance/GerritServer.java
@@ -330,14 +330,15 @@
TemporaryFolder temporaryFolder,
Description desc,
Config baseConfig,
- @Nullable Module testSysModule)
+ @Nullable Module testSysModule,
+ @Nullable Module testAuditModule)
throws Exception {
Path site = temporaryFolder.newFolder().toPath();
try {
if (!desc.memory()) {
init(desc, baseConfig, site);
}
- return start(desc, baseConfig, site, testSysModule, null);
+ return start(desc, baseConfig, site, testSysModule, testAuditModule, null);
} catch (Exception e) {
throw e;
}
@@ -365,6 +366,7 @@
Config baseConfig,
Path site,
@Nullable Module testSysModule,
+ @Nullable Module testAuditModule,
@Nullable InMemoryRepositoryManager inMemoryRepoManager,
String... additionalArgs)
throws Exception {
@@ -383,7 +385,8 @@
},
site);
daemon.setEmailModuleForTesting(new FakeEmailSender.Module());
- daemon.setAuditEventModuleForTesting(new FakeGroupAuditService.Module());
+ daemon.setAuditEventModuleForTesting(
+ MoreObjects.firstNonNull(testAuditModule, new FakeGroupAuditService.Module()));
if (testSysModule != null) {
daemon.addAdditionalSysModuleForTesting(testSysModule);
}
@@ -611,7 +614,7 @@
server.close();
server.daemon.stop();
- return start(server.desc, cfg, site, null, inMemoryRepoManager);
+ return start(server.desc, cfg, site, null, null, inMemoryRepoManager);
}
private static boolean hasBinding(Injector injector, Class<?> clazz) {
diff --git a/java/com/google/gerrit/acceptance/StandaloneSiteTest.java b/java/com/google/gerrit/acceptance/StandaloneSiteTest.java
index c38f5fa..43fe4eb 100644
--- a/java/com/google/gerrit/acceptance/StandaloneSiteTest.java
+++ b/java/com/google/gerrit/acceptance/StandaloneSiteTest.java
@@ -187,7 +187,7 @@
private GerritServer startImpl(@Nullable Module testSysModule, String... additionalArgs)
throws Exception {
return GerritServer.start(
- serverDesc, baseConfig, sitePaths.site_path, testSysModule, null, additionalArgs);
+ serverDesc, baseConfig, sitePaths.site_path, testSysModule, null, null, additionalArgs);
}
protected static void runGerrit(String... args) throws Exception {
diff --git a/java/com/google/gerrit/extensions/api/changes/RevertInput.java b/java/com/google/gerrit/extensions/api/changes/RevertInput.java
index c4272e4..148d24a 100644
--- a/java/com/google/gerrit/extensions/api/changes/RevertInput.java
+++ b/java/com/google/gerrit/extensions/api/changes/RevertInput.java
@@ -17,6 +17,10 @@
import com.google.gerrit.extensions.restapi.DefaultInput;
import java.util.Map;
+/**
+ * Input passed to {@code POST /changes/[change-id]/revert} and {@code POST
+ * /changes/[change-id]/revert_submission}
+ */
public class RevertInput {
@DefaultInput public String message;
@@ -26,4 +30,10 @@
public Map<RecipientType, NotifyInfo> notifyDetails;
public String topic;
+
+ /**
+ * Mark the change as work-in-progress. This will also override the {@link #notify} value to
+ * {@link NotifyHandling#OWNER}
+ */
+ public boolean workInProgress;
}
diff --git a/java/com/google/gerrit/pgm/Daemon.java b/java/com/google/gerrit/pgm/Daemon.java
index 64a6aa3..28407a7 100644
--- a/java/com/google/gerrit/pgm/Daemon.java
+++ b/java/com/google/gerrit/pgm/Daemon.java
@@ -491,12 +491,13 @@
modules.add(new AccountDeactivator.Module());
modules.add(new ChangeCleanupRunner.Module());
}
- modules.addAll(testSysModules);
modules.add(new LocalMergeSuperSetComputation.Module());
modules.add(new DefaultProjectNameLockManager.Module());
- return cfgInjector.createChildInjector(
- ModuleOverloader.override(
- modules, LibModuleLoader.loadModules(cfgInjector, LibModuleType.SYS_MODULE)));
+
+ List<Module> libModules = LibModuleLoader.loadModules(cfgInjector, LibModuleType.SYS_MODULE);
+ libModules.addAll(testSysModules);
+
+ return cfgInjector.createChildInjector(ModuleOverloader.override(modules, libModules));
}
private Module createIndexModule() {
diff --git a/java/com/google/gerrit/server/ModuleOverloader.java b/java/com/google/gerrit/server/ModuleOverloader.java
index 9a8fe84..6b7b082 100644
--- a/java/com/google/gerrit/server/ModuleOverloader.java
+++ b/java/com/google/gerrit/server/ModuleOverloader.java
@@ -42,7 +42,7 @@
return modules;
}
- // swipe cache implementation with alternative provided in lib
+ // swap module implementations with the matching alternative ones provided in lib
return modules.stream()
.map(
m -> {
diff --git a/java/com/google/gerrit/server/config/PluginConfig.java b/java/com/google/gerrit/server/config/PluginConfig.java
index 2459d0b..2b363f1 100644
--- a/java/com/google/gerrit/server/config/PluginConfig.java
+++ b/java/com/google/gerrit/server/config/PluginConfig.java
@@ -156,7 +156,7 @@
public Optional<GroupReference> getGroupReference(String name) {
String exactName = GroupReference.extractGroupName(getString(name));
- return groupReferences().values().stream().filter(g -> exactName.equals(g.getName())).findAny();
+ return groupReferences().values().stream().filter(g -> g.getName().equals(exactName)).findAny();
}
/** Mutable representation of {@link PluginConfig}. Used for updates. */
diff --git a/java/com/google/gerrit/server/git/CommitUtil.java b/java/com/google/gerrit/server/git/CommitUtil.java
index 7ddf16fd..67c7f01 100644
--- a/java/com/google/gerrit/server/git/CommitUtil.java
+++ b/java/com/google/gerrit/server/git/CommitUtil.java
@@ -266,6 +266,9 @@
RevCommit revertCommit = revWalk.parseCommit(revertCommitId);
Change changeToRevert = notes.getChange();
Change.Id changeId = Change.id(seq.nextChangeId());
+ if (input.workInProgress) {
+ input.notify = NotifyHandling.OWNER;
+ }
NotifyResolver.Result notify =
notifyResolver.resolve(firstNonNull(input.notify, NotifyHandling.ALL), input.notifyDetails);
@@ -285,6 +288,7 @@
ccs.remove(user.getAccountId());
ins.setReviewersAndCcs(reviewers, ccs);
ins.setRevertOf(notes.getChangeId());
+ ins.setWorkInProgress(input.workInProgress);
try (BatchUpdate bu = updateFactory.create(notes.getProjectName(), user, ts)) {
bu.setRepository(git, revWalk, oi);
diff --git a/java/com/google/gerrit/server/restapi/change/CherryPickChange.java b/java/com/google/gerrit/server/restapi/change/CherryPickChange.java
index 5fab976..fdac552 100644
--- a/java/com/google/gerrit/server/restapi/change/CherryPickChange.java
+++ b/java/com/google/gerrit/server/restapi/change/CherryPickChange.java
@@ -177,6 +177,7 @@
TimeUtil.nowTs(),
null,
null,
+ null,
null);
}
@@ -208,7 +209,7 @@
throws IOException, InvalidChangeOperationException, UpdateException, RestApiException,
ConfigInvalidException, NoSuchProjectException {
return cherryPick(
- sourceChange, project, sourceCommit, input, dest, TimeUtil.nowTs(), null, null, null);
+ sourceChange, project, sourceCommit, input, dest, TimeUtil.nowTs(), null, null, null, null);
}
/**
@@ -249,7 +250,8 @@
Timestamp timestamp,
@Nullable Change.Id revertedChange,
@Nullable ObjectId changeIdForNewChange,
- @Nullable Change.Id idForNewChange)
+ @Nullable Change.Id idForNewChange,
+ @Nullable Boolean workInProgress)
throws IOException, InvalidChangeOperationException, UpdateException, RestApiException,
ConfigInvalidException, NoSuchProjectException {
@@ -370,7 +372,8 @@
destChanges.get(0).notes(),
cherryPickCommit,
sourceChange.currentPatchSetId(),
- newTopic);
+ newTopic,
+ workInProgress);
} else {
// Change key not found on destination branch. We can create a new
// change.
@@ -385,7 +388,8 @@
sourceCommit,
input,
revertedChange,
- idForNewChange);
+ idForNewChange,
+ workInProgress);
}
bu.execute();
return Result.create(changeId, cherryPickCommit.getFilesWithGitConflicts());
@@ -453,13 +457,17 @@
ChangeNotes destNotes,
CodeReviewCommit cherryPickCommit,
PatchSet.Id sourcePatchSetId,
- String topic)
+ String topic,
+ @Nullable Boolean workInProgress)
throws IOException {
Change destChange = destNotes.getChange();
PatchSet.Id psId = ChangeUtil.nextPatchSetId(git, destChange.currentPatchSetId());
PatchSetInserter inserter = patchSetInserterFactory.create(destNotes, psId, cherryPickCommit);
inserter.setMessage("Uploaded patch set " + inserter.getPatchSetId().get() + ".");
inserter.setTopic(topic);
+ if (workInProgress != null) {
+ inserter.setWorkInProgress(workInProgress);
+ }
bu.addOp(destChange.getId(), inserter);
if (destChange.getCherryPickOf() == null
|| !destChange.getCherryPickOf().equals(sourcePatchSetId)) {
@@ -479,11 +487,19 @@
@Nullable ObjectId sourceCommit,
CherryPickInput input,
@Nullable Change.Id revertOf,
- @Nullable Change.Id idForNewChange)
+ @Nullable Change.Id idForNewChange,
+ @Nullable Boolean workInProgress)
throws IOException, InvalidChangeOperationException {
Change.Id changeId = idForNewChange != null ? idForNewChange : Change.id(seq.nextChangeId());
ChangeInserter ins = changeInserterFactory.create(changeId, cherryPickCommit, refName);
ins.setRevertOf(revertOf);
+ if (workInProgress != null) {
+ ins.setWorkInProgress(workInProgress);
+ } else {
+ ins.setWorkInProgress(
+ (sourceChange != null && sourceChange.isWorkInProgress())
+ || !cherryPickCommit.getFilesWithGitConflicts().isEmpty());
+ }
BranchNameKey sourceBranch = sourceChange == null ? null : sourceChange.getDest();
PatchSet.Id sourcePatchSetId = sourceChange == null ? null : sourceChange.currentPatchSetId();
ins.setMessage(
@@ -493,10 +509,7 @@
: "Uploaded patch set 1.") // For revert commits, the message should not include
// cherry-pick information.
.setTopic(topic)
- .setCherryPickOf(sourcePatchSetId)
- .setWorkInProgress(
- (sourceChange != null && sourceChange.isWorkInProgress())
- || !cherryPickCommit.getFilesWithGitConflicts().isEmpty());
+ .setCherryPickOf(sourcePatchSetId);
if (input.keepReviewers && sourceChange != null) {
ReviewerSet reviewerSet =
approvalsUtil.getReviewers(changeNotesFactory.createChecked(sourceChange));
diff --git a/java/com/google/gerrit/server/restapi/change/RevertSubmission.java b/java/com/google/gerrit/server/restapi/change/RevertSubmission.java
index 1f89b94..d128186 100644
--- a/java/com/google/gerrit/server/restapi/change/RevertSubmission.java
+++ b/java/com/google/gerrit/server/restapi/change/RevertSubmission.java
@@ -204,6 +204,7 @@
checkPermissionsForAllChanges(changeResource, changeDatas);
input.topic = createTopic(input.topic, submissionId);
+
return Response.ok(revertSubmission(changeDatas, input));
}
@@ -259,6 +260,9 @@
cherryPickInput.base = null;
Project.NameKey project = projectAndBranch.project();
cherryPickInput.destination = projectAndBranch.branch();
+ if (revertInput.workInProgress) {
+ cherryPickInput.notify = NotifyHandling.OWNER;
+ }
Collection<ChangeData> changesInProjectAndBranch =
changesPerProjectAndBranch.get(projectAndBranch);
@@ -333,7 +337,11 @@
bu.addOp(
changeNotes.getChange().getId(),
new CreateCherryPickOp(
- revCommitId, generatedChangeId, cherryPickRevertChangeId, timestamp));
+ revCommitId,
+ generatedChangeId,
+ cherryPickRevertChangeId,
+ timestamp,
+ revertInput.workInProgress));
bu.addOp(changeNotes.getChange().getId(), new PostRevertedMessageOp(generatedChangeId));
bu.addOp(
cherryPickRevertChangeId,
@@ -550,16 +558,19 @@
private final ObjectId computedChangeId;
private final Change.Id cherryPickRevertChangeId;
private final Timestamp timestamp;
+ private final boolean workInProgress;
CreateCherryPickOp(
ObjectId revCommitId,
ObjectId computedChangeId,
Change.Id cherryPickRevertChangeId,
- Timestamp timestamp) {
+ Timestamp timestamp,
+ Boolean workInProgress) {
this.revCommitId = revCommitId;
this.computedChangeId = computedChangeId;
this.cherryPickRevertChangeId = cherryPickRevertChangeId;
this.timestamp = timestamp;
+ this.workInProgress = workInProgress;
}
@Override
@@ -576,7 +587,8 @@
timestamp,
change.getId(),
computedChangeId,
- cherryPickRevertChangeId);
+ cherryPickRevertChangeId,
+ workInProgress);
// save the commit as base for next cherryPick of that branch
cherryPickInput.base =
changeNotesFactory
diff --git a/javatests/com/google/gerrit/acceptance/DaemonOverridesTestLibModulesIT.java b/javatests/com/google/gerrit/acceptance/DaemonOverridesTestLibModulesIT.java
new file mode 100644
index 0000000..f3a2324
--- /dev/null
+++ b/javatests/com/google/gerrit/acceptance/DaemonOverridesTestLibModulesIT.java
@@ -0,0 +1,62 @@
+// Copyright (C) 2020 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.acceptance;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.gerrit.server.ModuleImpl;
+import com.google.gerrit.server.audit.AuditModule;
+import com.google.inject.Inject;
+import com.google.inject.Module;
+import com.google.inject.name.Named;
+import com.google.inject.name.Names;
+import org.junit.Test;
+
+public class DaemonOverridesTestLibModulesIT extends AbstractDaemonTest {
+ private static final String TEST_MODULE = "test-module";
+
+ @Inject
+ @Named(value = TEST_MODULE)
+ private String testModuleClassName;
+
+ public abstract static class TestModule extends AuditModule {
+ @Override
+ protected void configure() {
+ super.configure();
+ bind(String.class).annotatedWith(Names.named(TEST_MODULE)).toInstance(getClass().getName());
+ }
+ }
+
+ @ModuleImpl(name = TEST_MODULE)
+ public static class DefaultModule extends TestModule {}
+
+ @ModuleImpl(name = TEST_MODULE)
+ public static class OverriddenModule extends TestModule {}
+
+ @Override
+ public Module createAuditModule() {
+ return new DefaultModule();
+ }
+
+ @Override
+ public Module createModule() {
+ return new OverriddenModule();
+ }
+
+ @Test
+ public void testSysModuleShouldOverrideTheDefaultOneWithSameModuleAnnotation() {
+ assertThat(testModuleClassName).isEqualTo(OverriddenModule.class.getName());
+ }
+}
diff --git a/javatests/com/google/gerrit/acceptance/api/change/RevertIT.java b/javatests/com/google/gerrit/acceptance/api/change/RevertIT.java
index 69278b4..a3a089f 100644
--- a/javatests/com/google/gerrit/acceptance/api/change/RevertIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/change/RevertIT.java
@@ -250,6 +250,17 @@
}
@Test
+ public void revertChangeWithWip() throws Exception {
+ PushOneCommit.Result r = createChange();
+ gApi.changes().id(r.getChangeId()).revision(r.getCommit().name()).review(ReviewInput.approve());
+ gApi.changes().id(r.getChangeId()).revision(r.getCommit().name()).submit();
+
+ RevertInput in = createWipRevertInput();
+ ChangeInfo revertChange = gApi.changes().id(r.getChangeId()).revert(in).get();
+ assertThat(revertChange.workInProgress).isTrue();
+ }
+
+ @Test
public void revertWithDefaultTopic() throws Exception {
PushOneCommit.Result result = createChange();
gApi.changes().id(result.getChangeId()).current().review(ReviewInput.approve());
@@ -321,6 +332,18 @@
}
@Test
+ public void revertNotificationsSupressedOnWip() throws Exception {
+ PushOneCommit.Result r = createChange();
+ gApi.changes().id(r.getChangeId()).addReviewer(user.email());
+ gApi.changes().id(r.getChangeId()).revision(r.getCommit().name()).review(ReviewInput.approve());
+ gApi.changes().id(r.getChangeId()).revision(r.getCommit().name()).submit();
+
+ sender.clear();
+ gApi.changes().id(r.getChangeId()).revert(createWipRevertInput()).get();
+ assertThat(sender.getMessages()).isEmpty();
+ }
+
+ @Test
public void suppressRevertNotifications() throws Exception {
PushOneCommit.Result r = createChange();
gApi.changes().id(r.getChangeId()).addReviewer(user.email());
@@ -659,6 +682,49 @@
}
@Test
+ public void revertSubmissionWipNotificationsAreSupressed() throws Exception {
+ String changeId1 = createChange("first change", "a.txt", "message").getChangeId();
+ approve(changeId1);
+ gApi.changes().id(changeId1).addReviewer(user.email());
+ String changeId2 = createChange("second change", "b.txt", "other").getChangeId();
+ approve(changeId2);
+ gApi.changes().id(changeId2).addReviewer(user.email());
+
+ gApi.changes().id(changeId2).current().submit();
+
+ sender.clear();
+
+ RevertInput revertInput = createWipRevertInput();
+ // Setting the Notifications to ALL will be overridden because the WIP flag overrides the
+ // notifications to OWNER
+ revertInput.notify = NotifyHandling.ALL;
+ gApi.changes().id(changeId2).revertSubmission(revertInput);
+
+ assertThat(sender.getMessages()).isEmpty();
+ }
+
+ @Test
+ public void revertSubmissionWipMarksAllChangesAsWip() throws Exception {
+ String changeId1 = createChange("first change", "a.txt", "message").getChangeId();
+ approve(changeId1);
+ gApi.changes().id(changeId1).addReviewer(user.email());
+ String changeId2 = createChange("second change", "b.txt", "other").getChangeId();
+ approve(changeId2);
+ gApi.changes().id(changeId2).addReviewer(user.email());
+
+ gApi.changes().id(changeId2).current().submit();
+
+ sender.clear();
+
+ RevertInput revertInput = createWipRevertInput();
+ RevertSubmissionInfo revertSubmissionInfo =
+ gApi.changes().id(changeId2).revertSubmission(revertInput);
+
+ assertThat(revertSubmissionInfo.revertChanges.stream().allMatch(r -> r.workInProgress))
+ .isTrue();
+ }
+
+ @Test
public void revertSubmissionIdenticalTreeIsAllowed() throws Exception {
String unrelatedChange = createChange("change1", "a.txt", "message").getChangeId();
approve(unrelatedChange);
@@ -1294,4 +1360,10 @@
}
return results;
}
+
+ private RevertInput createWipRevertInput() {
+ RevertInput input = new RevertInput();
+ input.workInProgress = true;
+ return input;
+ }
}
diff --git a/javatests/com/google/gerrit/acceptance/git/PushPermissionsIT.java b/javatests/com/google/gerrit/acceptance/git/PushPermissionsIT.java
index 9638658..1a2ae7c 100644
--- a/javatests/com/google/gerrit/acceptance/git/PushPermissionsIT.java
+++ b/javatests/com/google/gerrit/acceptance/git/PushPermissionsIT.java
@@ -82,14 +82,17 @@
.update();
}
+ @SuppressWarnings("TruthIncompatibleType")
@Test
public void mixingMagicAndRegularPush() throws Exception {
testRepo.branch("HEAD").commit().create();
PushResult r = push("HEAD:refs/heads/master", "HEAD:refs/for/master");
String msg = "cannot combine normal pushes and magic pushes";
- assertThat(r.getRemoteUpdate("refs/heads/master")).isNotEqualTo(Status.OK);
- assertThat(r.getRemoteUpdate("refs/for/master")).isNotEqualTo(Status.OK);
+ assertThat(r.getRemoteUpdate("refs/heads/master"))
+ .isNotEqualTo(/* expected: RemoteRefUpdate, actual: Status */ Status.OK);
+ assertThat(r.getRemoteUpdate("refs/for/master"))
+ .isNotEqualTo(/* expected: RemoteRefUpdate, actual: Status */ Status.OK);
assertThat(r.getRemoteUpdate("refs/for/master").getMessage()).isEqualTo(msg);
}
diff --git a/javatests/com/google/gerrit/server/cache/PerThreadCacheTest.java b/javatests/com/google/gerrit/server/cache/PerThreadCacheTest.java
index d19073d..5d420d3 100644
--- a/javatests/com/google/gerrit/server/cache/PerThreadCacheTest.java
+++ b/javatests/com/google/gerrit/server/cache/PerThreadCacheTest.java
@@ -21,12 +21,16 @@
import org.junit.Test;
public class PerThreadCacheTest {
+
+ @SuppressWarnings("TruthIncompatibleType")
@Test
public void key_respectsClass() {
assertThat(PerThreadCache.Key.create(String.class))
.isEqualTo(PerThreadCache.Key.create(String.class));
assertThat(PerThreadCache.Key.create(String.class))
- .isNotEqualTo(PerThreadCache.Key.create(Integer.class));
+ .isNotEqualTo(
+ /* expected: Key<String>, actual: Key<Integer> */ PerThreadCache.Key.create(
+ Integer.class));
}
@Test
diff --git a/javatests/com/google/gerrit/server/restapi/change/ListChangeCommentsTest.java b/javatests/com/google/gerrit/server/restapi/change/ListChangeCommentsTest.java
index daefd7c..5cefe74 100644
--- a/javatests/com/google/gerrit/server/restapi/change/ListChangeCommentsTest.java
+++ b/javatests/com/google/gerrit/server/restapi/change/ListChangeCommentsTest.java
@@ -32,6 +32,8 @@
@RunWith(JUnit4.class)
public class ListChangeCommentsTest {
+
+ @SuppressWarnings("TruthIncompatibleType")
@Test
public void commentsLinkedToChangeMessagesIgnoreGerritAutoGenTaggedMessages() {
/* Comments should not be linked to Gerrit's autogenerated messages */
@@ -54,7 +56,9 @@
// Make sure no comment is linked to the auto-gen message
assertThat(comments.stream().map(c -> c.changeMessageId).collect(Collectors.toSet()))
- .doesNotContain(getChangeMessage(changeMessages, "cmAutoGenByGerrit"));
+ .doesNotContain(
+ /* expected: String, actual: ChangeMessage */ getChangeMessage(
+ changeMessages, "cmAutoGenByGerrit"));
}
@Test
diff --git a/polygerrit-ui/app/elements/change-list/gr-user-header/gr-user-header.js b/polygerrit-ui/app/elements/change-list/gr-user-header/gr-user-header.js
deleted file mode 100644
index 7901c53..0000000
--- a/polygerrit-ui/app/elements/change-list/gr-user-header/gr-user-header.js
+++ /dev/null
@@ -1,116 +0,0 @@
-/**
- * @license
- * Copyright (C) 2017 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-import '../../../styles/shared-styles.js';
-import '../../plugins/gr-endpoint-decorator/gr-endpoint-decorator.js';
-import '../../plugins/gr-endpoint-param/gr-endpoint-param.js';
-import '../../shared/gr-avatar/gr-avatar.js';
-import '../../shared/gr-date-formatter/gr-date-formatter.js';
-import '../../shared/gr-rest-api-interface/gr-rest-api-interface.js';
-import '../../../styles/dashboard-header-styles.js';
-import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js';
-import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
-import {PolymerElement} from '@polymer/polymer/polymer-element.js';
-import {htmlTemplate} from './gr-user-header_html.js';
-import {GerritNav} from '../../core/gr-navigation/gr-navigation.js';
-
-/**
- * @extends PolymerElement
- */
-class GrUserHeader extends GestureEventListeners(
- LegacyElementMixin(
- PolymerElement)) {
- static get template() { return htmlTemplate; }
-
- static get is() { return 'gr-user-header'; }
-
- static get properties() {
- return {
- /** @type {?string} */
- userId: {
- type: String,
- observer: '_accountChanged',
- },
-
- showDashboardLink: {
- type: Boolean,
- value: false,
- },
-
- loggedIn: {
- type: Boolean,
- value: false,
- },
-
- /**
- * @type {?{name: ?, email: ?, registered_on: ?}}
- */
- _accountDetails: {
- type: Object,
- value: null,
- },
-
- /** @type {?string} */
- _status: {
- type: String,
- value: null,
- },
- };
- }
-
- _accountChanged(userId) {
- if (!userId) {
- this._accountDetails = null;
- this._status = null;
- return;
- }
-
- this.$.restAPI.getAccountDetails(userId).then(details => {
- this._accountDetails = details;
- });
- this.$.restAPI.getAccountStatus(userId).then(status => {
- this._status = status;
- });
- }
-
- _computeDisplayClass(status) {
- return status ? ' ' : 'hide';
- }
-
- _computeDetail(accountDetails, name) {
- return accountDetails ? accountDetails[name] : '';
- }
-
- _computeStatusClass(accountDetails) {
- return this._computeDetail(accountDetails, 'status') ? '' : 'hide';
- }
-
- _computeDashboardUrl(accountDetails) {
- if (!accountDetails) { return null; }
- const id = accountDetails._account_id;
- const email = accountDetails.email;
- if (!id && !email ) { return null; }
- return GerritNav.getUrlForUserDashboard(id ? id : email);
- }
-
- _computeDashboardLinkClass(showDashboardLink, loggedIn) {
- return showDashboardLink && loggedIn ?
- 'dashboardLink' : 'dashboardLink hide';
- }
-}
-
-customElements.define(GrUserHeader.is, GrUserHeader);
diff --git a/polygerrit-ui/app/elements/change-list/gr-user-header/gr-user-header.ts b/polygerrit-ui/app/elements/change-list/gr-user-header/gr-user-header.ts
new file mode 100644
index 0000000..25369b8
--- /dev/null
+++ b/polygerrit-ui/app/elements/change-list/gr-user-header/gr-user-header.ts
@@ -0,0 +1,113 @@
+/**
+ * @license
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import '../../../styles/shared-styles';
+import '../../plugins/gr-endpoint-decorator/gr-endpoint-decorator';
+import '../../plugins/gr-endpoint-param/gr-endpoint-param';
+import '../../shared/gr-avatar/gr-avatar';
+import '../../shared/gr-date-formatter/gr-date-formatter';
+import '../../shared/gr-rest-api-interface/gr-rest-api-interface';
+import '../../../styles/dashboard-header-styles';
+import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners';
+import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin';
+import {PolymerElement} from '@polymer/polymer/polymer-element';
+import {htmlTemplate} from './gr-user-header_html';
+import {GerritNav} from '../../core/gr-navigation/gr-navigation';
+import {customElement, property} from '@polymer/decorators';
+import {RestApiService} from '../../../services/services/gr-rest-api/gr-rest-api';
+import {AccountDetailInfo, AccountId} from '../../../types/common';
+
+export interface GrUserHeader {
+ $: {
+ restAPI: RestApiService & Element;
+ };
+}
+
+@customElement('gr-user-header')
+export class GrUserHeader extends GestureEventListeners(
+ LegacyElementMixin(PolymerElement)
+) {
+ static get template() {
+ return htmlTemplate;
+ }
+
+ @property({type: String, observer: '_accountChanged'})
+ userId?: AccountId;
+
+ @property({type: Boolean})
+ showDashboardLink = false;
+
+ @property({type: Boolean})
+ loggedIn = false;
+
+ @property({type: Object})
+ _accountDetails: AccountDetailInfo | null = null;
+
+ @property({type: String})
+ _status = '';
+
+ _accountChanged(userId?: AccountId) {
+ if (!userId) {
+ this._accountDetails = null;
+ this._status = '';
+ return;
+ }
+
+ this.$.restAPI.getAccountDetails(userId).then(details => {
+ this._accountDetails = details ?? null;
+ this._status = details?.status ?? '';
+ });
+ }
+
+ _computeDetail(
+ accountDetails: AccountDetailInfo | null,
+ name: keyof AccountDetailInfo
+ ) {
+ return accountDetails ? accountDetails[name] : '';
+ }
+
+ _computeStatusClass(status: string) {
+ return status ? '' : 'hide';
+ }
+
+ _computeDashboardUrl(accountDetails: AccountDetailInfo | null) {
+ if (!accountDetails) {
+ return null;
+ }
+ const id = accountDetails._account_id;
+ if (id) {
+ return GerritNav.getUrlForUserDashboard(String(id));
+ }
+ const email = accountDetails.email;
+ if (email) {
+ return GerritNav.getUrlForUserDashboard(email);
+ }
+ return null;
+ }
+
+ _computeDashboardLinkClass(showDashboardLink: boolean, loggedIn: boolean) {
+ return showDashboardLink && loggedIn
+ ? 'dashboardLink'
+ : 'dashboardLink hide';
+ }
+}
+
+declare global {
+ interface HTMLElementTagNameMap {
+ 'gr-user-header': GrUserHeader;
+ }
+}
diff --git a/polygerrit-ui/app/elements/change-list/gr-user-header/gr-user-header_html.ts b/polygerrit-ui/app/elements/change-list/gr-user-header/gr-user-header_html.ts
index 72bdca6..136835d 100644
--- a/polygerrit-ui/app/elements/change-list/gr-user-header/gr-user-header_html.ts
+++ b/polygerrit-ui/app/elements/change-list/gr-user-header/gr-user-header_html.ts
@@ -37,7 +37,7 @@
[[_computeDetail(_accountDetails, 'name')]]
</h1>
<hr />
- <div class$="status [[_computeStatusClass(_accountDetails)]]">
+ <div class$="status [[_computeStatusClass(_status)]]">
<span>Status:</span> [[_status]]
</div>
<div>
diff --git a/polygerrit-ui/app/elements/change-list/gr-user-header/gr-user-header_test.js b/polygerrit-ui/app/elements/change-list/gr-user-header/gr-user-header_test.js
index 6baacef..15fbf8b 100644
--- a/polygerrit-ui/app/elements/change-list/gr-user-header/gr-user-header_test.js
+++ b/polygerrit-ui/app/elements/change-list/gr-user-header/gr-user-header_test.js
@@ -32,10 +32,9 @@
.returns(Promise.resolve({
name: 'foo',
email: 'bar',
+ status: 'OOO',
registered_on: '2015-03-12 18:32:08.000000000',
}));
- sinon.stub(element.$.restAPI, 'getAccountStatus')
- .returns(Promise.resolve('baz'));
element.userId = 'foo.bar@baz';
flush(() => {
@@ -46,7 +45,7 @@
flush(() => {
flushAsynchronousOperations();
assert.isNull(element._accountDetails);
- assert.isNull(element._status);
+ assert.equal(element._status, '');
done();
});
diff --git a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_html.ts b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_html.ts
index b311efc..f0b8372 100644
--- a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_html.ts
+++ b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_html.ts
@@ -753,6 +753,7 @@
class="scrollable"
no-cancel-on-outside-click=""
no-cancel-on-esc-key=""
+ scroll-action="lock"
with-backdrop=""
>
<gr-reply-dialog
diff --git a/polygerrit-ui/app/elements/change/gr-message/gr-message.js b/polygerrit-ui/app/elements/change/gr-message/gr-message.js
deleted file mode 100644
index 0da687f..0000000
--- a/polygerrit-ui/app/elements/change/gr-message/gr-message.js
+++ /dev/null
@@ -1,430 +0,0 @@
-/**
- * @license
- * Copyright (C) 2015 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-import '@polymer/iron-icon/iron-icon.js';
-import '../../shared/gr-account-label/gr-account-label.js';
-import '../../shared/gr-account-chip/gr-account-chip.js';
-import '../../shared/gr-button/gr-button.js';
-import '../../shared/gr-date-formatter/gr-date-formatter.js';
-import '../../shared/gr-formatted-text/gr-formatted-text.js';
-import '../../shared/gr-rest-api-interface/gr-rest-api-interface.js';
-import '../../../styles/shared-styles.js';
-import '../../../styles/gr-voting-styles.js';
-import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js';
-import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
-import {PolymerElement} from '@polymer/polymer/polymer-element.js';
-import {htmlTemplate} from './gr-message_html.js';
-import {SpecialFilePath} from '../../../constants/constants.js';
-
-const PATCH_SET_PREFIX_PATTERN = /^Patch Set \d+:\s*(.*)/;
-const LABEL_TITLE_SCORE_PATTERN = /^(-?)([A-Za-z0-9-]+?)([+-]\d+)?$/;
-
-/**
- * @extends PolymerElement
- */
-class GrMessage extends GestureEventListeners(
- LegacyElementMixin(PolymerElement)) {
- static get template() { return htmlTemplate; }
-
- static get is() { return 'gr-message'; }
- /**
- * Fired when this message's reply link is tapped.
- *
- * @event reply
- */
-
- /**
- * Fired when the message's timestamp is tapped.
- *
- * @event message-anchor-tap
- */
-
- /**
- * Fired when a change message is deleted.
- *
- * @event change-message-deleted
- */
-
- static get properties() {
- return {
- /** @type {?} */
- change: Object,
- changeNum: Number,
- /** @type {?} */
- message: Object,
- author: {
- type: Object,
- computed: '_computeAuthor(message)',
- },
- /**
- * TODO(taoalpha): remove once the change log experiment is launched
- *
- * @type {Object} - a map on file and comments on it
- */
- comments: {
- type: Object,
- },
- config: Object,
- hideAutomated: {
- type: Boolean,
- value: false,
- },
- hidden: {
- type: Boolean,
- computed: '_computeIsHidden(hideAutomated, isAutomated)',
- reflectToAttribute: true,
- },
- isAutomated: {
- type: Boolean,
- computed: '_computeIsAutomated(message)',
- },
- showOnBehalfOf: {
- type: Boolean,
- computed: '_computeShowOnBehalfOf(message)',
- },
- showReplyButton: {
- type: Boolean,
- computed: '_computeShowReplyButton(message, _loggedIn)',
- },
- projectName: {
- type: String,
- observer: '_projectNameChanged',
- },
-
- /**
- * A mapping from label names to objects representing the minimum and
- * maximum possible values for that label.
- */
- labelExtremes: Object,
-
- /**
- * @type {{ commentlinks: Array }}
- */
- _projectConfig: Object,
- // Computed property needed to trigger Polymer value observing.
- _expanded: {
- type: Object,
- computed: '_computeExpanded(message.expanded)',
- },
- _messageContentExpanded: {
- type: String,
- computed:
- '_computeMessageContentExpanded(message.message, message.tag)',
- },
- _messageContentCollapsed: {
- type: String,
- computed:
- '_computeMessageContentCollapsed(message.message, message.tag,' +
- ' message.commentThreads)',
- },
- _commentCountText: {
- type: Number,
- computed: '_computeCommentCountText(message.commentThreads.length)',
- },
- _loggedIn: {
- type: Boolean,
- value: false,
- },
- _isAdmin: {
- type: Boolean,
- value: false,
- },
- _isDeletingChangeMsg: {
- type: Boolean,
- value: false,
- },
- };
- }
-
- static get observers() {
- return [
- '_updateExpandedClass(message.expanded)',
- ];
- }
-
- constructor() {
- super();
- }
-
- /** @override */
- created() {
- super.created();
- this.addEventListener('click',
- e => this._handleClick(e));
- }
-
- /** @override */
- ready() {
- super.ready();
- this.$.restAPI.getConfig().then(config => {
- this.config = config;
- });
- this.$.restAPI.getLoggedIn().then(loggedIn => {
- this._loggedIn = loggedIn;
- });
- this.$.restAPI.getIsAdmin().then(isAdmin => {
- this._isAdmin = isAdmin;
- });
- }
-
- _updateExpandedClass(expanded) {
- if (expanded) {
- this.classList.add('expanded');
- } else {
- this.classList.remove('expanded');
- }
- }
-
- _computeCommentCountText(threadsLength) {
- if (threadsLength === 0) {
- return undefined;
- } else if (threadsLength === 1) {
- return '1 comment';
- } else {
- return `${threadsLength} comments`;
- }
- }
-
- _onThreadListModified() {
- // TODO(taoalpha): this won't propagate the changes to the files
- // should consider replacing this with either top level events
- // or gerrit level events
-
- // emit the event so change-view can also get updated with latest changes
- this.dispatchEvent(new CustomEvent('comment-refresh', {
- composed: true, bubbles: true,
- }));
- }
-
- _computeMessageContentExpanded(content, tag) {
- return this._computeMessageContent(content, tag, true);
- }
-
- _patchsetCommentSummary(commentThreads) {
- const id = this.message.id;
- if (!id) return '';
- const patchsetThreads = commentThreads.filter(thread =>
- thread.path === SpecialFilePath.PATCHSET_LEVEL_COMMENTS);
- for (const thread of patchsetThreads) {
- // Find if there was a patchset level comment created through the reply
- // dialog and use it to determine the summary
- if (thread.comments[0].change_message_id === id) {
- return thread.comments[0].message;
- }
- }
- // Find if there is a reply to some patchset comment left
- for (const thread of patchsetThreads) {
- for (const comment of thread.comments) {
- if (comment.change_message_id === id) { return comment.message; }
- }
- }
- return '';
- }
-
- _computeMessageContentCollapsed(content, tag, commentThreads) {
- const summary =
- this._computeMessageContent(content, tag, false);
- if (summary || !commentThreads) return summary;
- return this._patchsetCommentSummary(commentThreads);
- }
-
- _computeMessageContent(content, tag, isExpanded) {
- content = content || '';
- tag = tag || '';
- const isNewPatchSet = tag.endsWith(':newPatchSet') ||
- tag.endsWith(':newWipPatchSet');
- const lines = content.split('\n');
- const filteredLines = lines.filter(line => {
- if (!isExpanded && line.startsWith('>')) {
- return false;
- }
- if (line.startsWith('(') && line.endsWith(' comment)')) {
- return false;
- }
- if (line.startsWith('(') && line.endsWith(' comments)')) {
- return false;
- }
- if (!isNewPatchSet && line.match(PATCH_SET_PREFIX_PATTERN)) {
- return false;
- }
- return true;
- });
- const mappedLines = filteredLines.map(line => {
- // The change message formatting is not very consistent, so
- // unfortunately we have to do a bit of tweaking here:
- // Labels should be stripped from lines like this:
- // Patch Set 29: Verified+1
- // Rebase messages (which have a ':newPatchSet' tag) should be kept on
- // lines like this:
- // Patch Set 27: Patch Set 26 was rebased
- if (isNewPatchSet) {
- line = line.replace(PATCH_SET_PREFIX_PATTERN, '$1');
- }
- return line;
- });
- return mappedLines.join('\n').trim();
- }
-
- _computeAuthor(message) {
- return message.author || message.updated_by;
- }
-
- _computeShowOnBehalfOf(message) {
- const author = message.author || message.updated_by;
- return !!(author && message.real_author &&
- author._account_id != message.real_author._account_id);
- }
-
- _computeShowReplyButton(message, loggedIn) {
- return message && !!message.message && loggedIn &&
- !this._computeIsAutomated(message);
- }
-
- _computeExpanded(expanded) {
- return expanded;
- }
-
- _handleClick(e) {
- if (this.message.expanded) { return; }
- e.stopPropagation();
- this.set('message.expanded', true);
- }
-
- _handleAuthorClick(e) {
- if (!this.message.expanded) { return; }
- e.stopPropagation();
- this.set('message.expanded', false);
- }
-
- _computeIsAutomated(message) {
- return !!(message.reviewer ||
- this._computeIsReviewerUpdate(message) ||
- (message.tag && message.tag.startsWith('autogenerated')));
- }
-
- _computeIsHidden(hideAutomated, isAutomated) {
- return hideAutomated && isAutomated;
- }
-
- _computeIsReviewerUpdate(message) {
- return message.type === 'REVIEWER_UPDATE';
- }
-
- _getScores(message, labelExtremes) {
- if (!message || !message.message || !labelExtremes) {
- return [];
- }
- const line = message.message.split('\n', 1)[0];
- const patchSetPrefix = PATCH_SET_PREFIX_PATTERN;
- if (!line.match(patchSetPrefix)) {
- return [];
- }
- const scoresRaw = line.split(patchSetPrefix)[1];
- if (!scoresRaw) {
- return [];
- }
- return scoresRaw.split(' ')
- .map(s => s.match(LABEL_TITLE_SCORE_PATTERN))
- .filter(ms =>
- ms && ms.length === 4 && labelExtremes.hasOwnProperty(ms[2]))
- .map(ms => {
- const label = ms[2];
- const value = ms[1] === '-' ? 'removed' : ms[3];
- return {label, value};
- });
- }
-
- _computeScoreClass(score, labelExtremes) {
- // Polymer 2: check for undefined
- if ([score, labelExtremes].includes(undefined)) {
- return '';
- }
- if (score.value === 'removed') {
- return 'removed';
- }
- const classes = [];
- if (score.value > 0) {
- classes.push('positive');
- } else if (score.value < 0) {
- classes.push('negative');
- }
- const extremes = labelExtremes[score.label];
- if (extremes) {
- const intScore = parseInt(score.value, 10);
- if (intScore === extremes.max) {
- classes.push('max');
- } else if (intScore === extremes.min) {
- classes.push('min');
- }
- }
- return classes.join(' ');
- }
-
- _computeClass(expanded) {
- const classes = [];
- classes.push(expanded ? 'expanded' : 'collapsed');
- return classes.join(' ');
- }
-
- _handleAnchorClick(e) {
- e.preventDefault();
- this.dispatchEvent(new CustomEvent('message-anchor-tap', {
- bubbles: true,
- composed: true,
- detail: {id: this.message.id},
- }));
- }
-
- _handleReplyTap(e) {
- e.preventDefault();
- this.dispatchEvent(new CustomEvent('reply', {
- detail: {message: this.message},
- composed: true, bubbles: true,
- }));
- }
-
- _handleDeleteMessage(e) {
- e.preventDefault();
- if (!this.message || !this.message.id) return;
- this._isDeletingChangeMsg = true;
- this.$.restAPI.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,
- }));
- });
- }
-
- _projectNameChanged(name) {
- this.$.restAPI.getProjectConfig(name).then(config => {
- this._projectConfig = config;
- });
- }
-
- _computeExpandToggleIcon(expanded) {
- return expanded ? 'gr-icons:expand-less' : 'gr-icons:expand-more';
- }
-
- _toggleExpanded(e) {
- e.stopPropagation();
- this.set('message.expanded', !this.message.expanded);
- }
-}
-
-customElements.define(GrMessage.is, GrMessage);
diff --git a/polygerrit-ui/app/elements/change/gr-message/gr-message.ts b/polygerrit-ui/app/elements/change/gr-message/gr-message.ts
new file mode 100644
index 0000000..2b5d94d
--- /dev/null
+++ b/polygerrit-ui/app/elements/change/gr-message/gr-message.ts
@@ -0,0 +1,493 @@
+/**
+ * @license
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import '@polymer/iron-icon/iron-icon';
+import '../../shared/gr-account-label/gr-account-label';
+import '../../shared/gr-account-chip/gr-account-chip';
+import '../../shared/gr-button/gr-button';
+import '../../shared/gr-date-formatter/gr-date-formatter';
+import '../../shared/gr-formatted-text/gr-formatted-text';
+import '../../shared/gr-rest-api-interface/gr-rest-api-interface';
+import '../../../styles/shared-styles';
+import '../../../styles/gr-voting-styles';
+import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners';
+import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin';
+import {PolymerElement} from '@polymer/polymer/polymer-element';
+import {htmlTemplate} from './gr-message_html';
+import {SpecialFilePath} from '../../../constants/constants';
+import {customElement, property, computed, observe} from '@polymer/decorators';
+import {
+ ChangeInfo,
+ ChangeMessageInfo,
+ ServerInfo,
+ ConfigInfo,
+ RepoName,
+ ReviewInputTag,
+ VotingRangeInfo,
+} from '../../../types/common';
+import {RestApiService} from '../../../services/services/gr-rest-api/gr-rest-api';
+import {CommentThread} from '../../diff/gr-comment-api/gr-comment-api';
+import {hasOwnProperty} from '../../../utils/common-util';
+
+const PATCH_SET_PREFIX_PATTERN = /^Patch Set \d+:\s*(.*)/;
+const LABEL_TITLE_SCORE_PATTERN = /^(-?)([A-Za-z0-9-]+?)([+-]\d+)?$/;
+
+declare global {
+ interface HTMLElementTagNameMap {
+ 'gr-message': GrMessage;
+ }
+}
+
+export interface GrMessage {
+ $: {
+ restAPI: RestApiService & Element;
+ };
+}
+
+interface ChangeMessage extends ChangeMessageInfo {
+ // TODO(TS): maybe should be an enum instead
+ type: string;
+ expanded: boolean;
+ commentThreads: CommentThread[];
+}
+
+export type LabelExtreme = {[labelName: string]: VotingRangeInfo};
+
+interface Score {
+ label?: string;
+ value?: string;
+}
+
+@customElement('gr-message')
+export class GrMessage extends GestureEventListeners(
+ LegacyElementMixin(PolymerElement)
+) {
+ static get template() {
+ return htmlTemplate;
+ }
+
+ /**
+ * Fired when this message's reply link is tapped.
+ *
+ * @event reply
+ */
+
+ /**
+ * Fired when the message's timestamp is tapped.
+ *
+ * @event message-anchor-tap
+ */
+
+ /**
+ * Fired when a change message is deleted.
+ *
+ * @event change-message-deleted
+ */
+
+ @property({type: Object})
+ change?: ChangeInfo;
+
+ @property({type: Number})
+ changeNum?: number;
+
+ @property({type: Object})
+ message: ChangeMessage | undefined;
+
+ @computed('message')
+ get author() {
+ return this.message?.author || this.message?.updated_by;
+ }
+
+ @property({type: Object})
+ config?: ServerInfo;
+
+ @property({type: Boolean})
+ hideAutomated = false;
+
+ @property({
+ type: Boolean,
+ reflectToAttribute: true,
+ computed: '_computeIsHidden(hideAutomated, isAutomated)',
+ })
+ hidden = false;
+
+ @computed('message')
+ get isAutomated() {
+ return !!this.message && this._computeIsAutomated(this.message);
+ }
+
+ @computed('message')
+ get showOnBehalfOf() {
+ return !!this.message && this._computeShowOnBehalfOf(this.message);
+ }
+
+ @property({
+ type: Boolean,
+ computed: '_computeShowReplyButton(message, _loggedIn)',
+ })
+ showReplyButton = false;
+
+ @property({type: String})
+ projectName?: string;
+
+ /**
+ * A mapping from label names to objects representing the minimum and
+ * maximum possible values for that label.
+ */
+ @property({type: Object})
+ labelExtremes?: LabelExtreme;
+
+ @property({type: Object})
+ _projectConfig?: ConfigInfo;
+
+ @property({type: Boolean})
+ _loggedIn = false;
+
+ @property({type: Boolean})
+ _isAdmin = false;
+
+ @property({type: Boolean})
+ _isDeletingChangeMsg = false;
+
+ @property({type: Boolean, computed: '_computeExpanded(message.expanded)'})
+ _expanded = false;
+
+ @property({
+ type: String,
+ computed: '_computeMessageContentExpanded(message.message, message.tag)',
+ })
+ _messageContentExpanded = '';
+
+ @property({
+ type: String,
+ computed:
+ '_computeMessageContentCollapsed(message.message, message.tag,' +
+ ' message.commentThreads)',
+ })
+ _messageContentCollapsed = '';
+
+ @property({
+ type: String,
+ computed: '_computeCommentCountText(message.commentThreads.length)',
+ })
+ _commentCountText = '';
+
+ created() {
+ super.created();
+ this.addEventListener('click', e => this._handleClick(e));
+ }
+
+ attached() {
+ super.attached();
+ this.$.restAPI.getConfig().then(config => {
+ this.config = config;
+ });
+ this.$.restAPI.getLoggedIn().then(loggedIn => {
+ this._loggedIn = loggedIn;
+ });
+ this.$.restAPI.getIsAdmin().then(isAdmin => {
+ this._isAdmin = !!isAdmin;
+ });
+ }
+
+ @observe('message.expanded')
+ _updateExpandedClass(expanded: boolean) {
+ if (expanded) {
+ this.classList.add('expanded');
+ } else {
+ this.classList.remove('expanded');
+ }
+ }
+
+ _computeCommentCountText(threadsLength?: number) {
+ if (threadsLength === 0) {
+ return undefined;
+ } else if (threadsLength === 1) {
+ return '1 comment';
+ } else {
+ return `${threadsLength} comments`;
+ }
+ }
+
+ _onThreadListModified() {
+ // TODO(taoalpha): this won't propagate the changes to the files
+ // should consider replacing this with either top level events
+ // or gerrit level events
+
+ // emit the event so change-view can also get updated with latest changes
+ this.dispatchEvent(
+ new CustomEvent('comment-refresh', {
+ composed: true,
+ bubbles: true,
+ })
+ );
+ }
+
+ _computeMessageContentExpanded(content?: string, tag?: ReviewInputTag) {
+ return this._computeMessageContent(content, tag, true);
+ }
+
+ _patchsetCommentSummary(commentThreads: CommentThread[] = []) {
+ const id = this.message?.id;
+ if (!id) return '';
+ const patchsetThreads = commentThreads.filter(
+ thread => thread.path === SpecialFilePath.PATCHSET_LEVEL_COMMENTS
+ );
+ for (const thread of patchsetThreads) {
+ // Find if there was a patchset level comment created through the reply
+ // dialog and use it to determine the summary
+ if (thread.comments[0].change_message_id === id) {
+ return thread.comments[0].message;
+ }
+ }
+ // Find if there is a reply to some patchset comment left
+ for (const thread of patchsetThreads) {
+ for (const comment of thread.comments) {
+ if (comment.change_message_id === id) {
+ return comment.message;
+ }
+ }
+ }
+ return '';
+ }
+
+ _computeMessageContentCollapsed(
+ content?: string,
+ tag?: ReviewInputTag,
+ commentThreads?: CommentThread[]
+ ) {
+ const summary = this._computeMessageContent(content, tag, false);
+ if (summary || !commentThreads) return summary;
+ return this._patchsetCommentSummary(commentThreads);
+ }
+
+ _computeMessageContent(
+ content = '',
+ tag: ReviewInputTag = '' as ReviewInputTag,
+ isExpanded: boolean
+ ) {
+ const isNewPatchSet =
+ tag.endsWith(':newPatchSet') || tag.endsWith(':newWipPatchSet');
+ const lines = content.split('\n');
+ const filteredLines = lines.filter(line => {
+ if (!isExpanded && line.startsWith('>')) {
+ return false;
+ }
+ if (line.startsWith('(') && line.endsWith(' comment)')) {
+ return false;
+ }
+ if (line.startsWith('(') && line.endsWith(' comments)')) {
+ return false;
+ }
+ if (!isNewPatchSet && line.match(PATCH_SET_PREFIX_PATTERN)) {
+ return false;
+ }
+ return true;
+ });
+ const mappedLines = filteredLines.map(line => {
+ // The change message formatting is not very consistent, so
+ // unfortunately we have to do a bit of tweaking here:
+ // Labels should be stripped from lines like this:
+ // Patch Set 29: Verified+1
+ // Rebase messages (which have a ':newPatchSet' tag) should be kept on
+ // lines like this:
+ // Patch Set 27: Patch Set 26 was rebased
+ if (isNewPatchSet) {
+ line = line.replace(PATCH_SET_PREFIX_PATTERN, '$1');
+ }
+ return line;
+ });
+ return mappedLines.join('\n').trim();
+ }
+
+ _computeAuthor(message: ChangeMessage) {
+ return message.author || message.updated_by;
+ }
+
+ _computeShowOnBehalfOf(message: ChangeMessage) {
+ const author = this._computeAuthor(message);
+ return !!(
+ author &&
+ message.real_author &&
+ author._account_id !== message.real_author._account_id
+ );
+ }
+
+ _computeShowReplyButton(message?: ChangeMessage, loggedIn?: boolean) {
+ return (
+ message &&
+ !!message.message &&
+ loggedIn &&
+ !this._computeIsAutomated(message)
+ );
+ }
+
+ _computeExpanded(expanded: boolean) {
+ return expanded;
+ }
+
+ _handleClick(e: Event) {
+ if (this.message?.expanded) {
+ return;
+ }
+ e.stopPropagation();
+ this.set('message.expanded', true);
+ }
+
+ _handleAuthorClick(e: Event) {
+ if (!this.message?.expanded) {
+ return;
+ }
+ e.stopPropagation();
+ this.set('message.expanded', false);
+ }
+
+ _computeIsAutomated(message: ChangeMessage) {
+ return !!(
+ message.reviewer ||
+ this._computeIsReviewerUpdate(message) ||
+ (message.tag && message.tag.startsWith('autogenerated'))
+ );
+ }
+
+ _computeIsHidden(hideAutomated: boolean, isAutomated: boolean) {
+ return hideAutomated && isAutomated;
+ }
+
+ _computeIsReviewerUpdate(message: ChangeMessage) {
+ return message.type === 'REVIEWER_UPDATE';
+ }
+
+ _getScores(message?: ChangeMessage, labelExtremes?: LabelExtreme): Score[] {
+ if (!message || !message.message || !labelExtremes) {
+ return [];
+ }
+ const line = message.message.split('\n', 1)[0];
+ const patchSetPrefix = PATCH_SET_PREFIX_PATTERN;
+ if (!line.match(patchSetPrefix)) {
+ return [];
+ }
+ const scoresRaw = line.split(patchSetPrefix)[1];
+ if (!scoresRaw) {
+ return [];
+ }
+ return scoresRaw
+ .split(' ')
+ .map(s => s.match(LABEL_TITLE_SCORE_PATTERN))
+ .filter(
+ ms => ms && ms.length === 4 && hasOwnProperty(labelExtremes, ms[2])
+ )
+ .map(ms => {
+ const label = ms?.[2];
+ const value = ms?.[1] === '-' ? 'removed' : ms?.[3];
+ return {label, value};
+ });
+ }
+
+ _computeScoreClass(score?: Score, labelExtremes?: LabelExtreme) {
+ // Polymer 2: check for undefined
+ if (score === undefined || labelExtremes === undefined) {
+ return '';
+ }
+ if (!score.value) {
+ return '';
+ }
+ if (score.value === 'removed') {
+ return 'removed';
+ }
+ const classes = [];
+ if (Number(score.value) > 0) {
+ classes.push('positive');
+ } else if (Number(score.value) < 0) {
+ classes.push('negative');
+ }
+ if (score.label) {
+ const extremes = labelExtremes[score.label];
+ if (extremes) {
+ const intScore = Number(score.value);
+ if (intScore === extremes.max) {
+ classes.push('max');
+ } else if (intScore === extremes.min) {
+ classes.push('min');
+ }
+ }
+ }
+ return classes.join(' ');
+ }
+
+ _computeClass(expanded: boolean) {
+ const classes = [];
+ classes.push(expanded ? 'expanded' : 'collapsed');
+ return classes.join(' ');
+ }
+
+ _handleAnchorClick(e: Event) {
+ e.preventDefault();
+ this.dispatchEvent(
+ new CustomEvent('message-anchor-tap', {
+ bubbles: true,
+ composed: true,
+ detail: {id: this.message?.id},
+ })
+ );
+ }
+
+ _handleReplyTap(e: Event) {
+ e.preventDefault();
+ this.dispatchEvent(
+ new CustomEvent('reply', {
+ detail: {message: this.message},
+ composed: true,
+ bubbles: true,
+ })
+ );
+ }
+
+ _handleDeleteMessage(e: Event) {
+ e.preventDefault();
+ if (!this.message || !this.message.id || !this.changeNum) return;
+ this._isDeletingChangeMsg = true;
+ this.$.restAPI
+ .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,
+ })
+ );
+ });
+ }
+
+ @observe('projectName')
+ _projectNameChanged(name: string) {
+ this.$.restAPI.getProjectConfig(name as RepoName).then(config => {
+ this._projectConfig = config;
+ });
+ }
+
+ _computeExpandToggleIcon(expanded: boolean) {
+ return expanded ? 'gr-icons:expand-less' : 'gr-icons:expand-more';
+ }
+
+ _toggleExpanded(e: Event) {
+ e.stopPropagation();
+ this.set('message.expanded', !this.message?.expanded);
+ }
+}
diff --git a/polygerrit-ui/app/elements/change/gr-message/gr-message_test.js b/polygerrit-ui/app/elements/change/gr-message/gr-message_test.js
index c617d2c..66b8ed2 100644
--- a/polygerrit-ui/app/elements/change/gr-message/gr-message_test.js
+++ b/polygerrit-ui/app/elements/change/gr-message/gr-message_test.js
@@ -80,6 +80,7 @@
});
test('delete change message', done => {
+ element.changeNum = 314159;
element.message = {
id: '47c43261_55aa2c41',
author: {
diff --git a/polygerrit-ui/app/elements/core/gr-account-dropdown/gr-account-dropdown.js b/polygerrit-ui/app/elements/core/gr-account-dropdown/gr-account-dropdown.js
deleted file mode 100644
index 7608137..0000000
--- a/polygerrit-ui/app/elements/core/gr-account-dropdown/gr-account-dropdown.js
+++ /dev/null
@@ -1,132 +0,0 @@
-/**
- * @license
- * Copyright (C) 2015 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-import '../../shared/gr-button/gr-button.js';
-import '../../shared/gr-dropdown/gr-dropdown.js';
-import '../../shared/gr-rest-api-interface/gr-rest-api-interface.js';
-import '../../../styles/shared-styles.js';
-import '../../shared/gr-avatar/gr-avatar.js';
-import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js';
-import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
-import {PolymerElement} from '@polymer/polymer/polymer-element.js';
-import {htmlTemplate} from './gr-account-dropdown_html.js';
-import {getUserName} from '../../../utils/display-name-util.js';
-
-const INTERPOLATE_URL_PATTERN = /\${([\w]+)}/g;
-
-/**
- * @extends PolymerElement
- */
-class GrAccountDropdown extends GestureEventListeners(
- LegacyElementMixin(
- PolymerElement)) {
- static get template() { return htmlTemplate; }
-
- static get is() { return 'gr-account-dropdown'; }
-
- static get properties() {
- return {
- account: Object,
- config: Object,
- links: {
- type: Array,
- computed: '_getLinks(_switchAccountUrl, _path)',
- },
- topContent: {
- type: Array,
- computed: '_getTopContent(account)',
- },
- _path: {
- type: String,
- value: '/',
- },
- _hasAvatars: Boolean,
- _switchAccountUrl: String,
- };
- }
-
- /** @override */
- attached() {
- super.attached();
- this._handleLocationChange();
- this.listen(window, 'location-change', '_handleLocationChange');
- this.$.restAPI.getConfig().then(cfg => {
- this.config = cfg;
-
- if (cfg && cfg.auth && cfg.auth.switch_account_url) {
- this._switchAccountUrl = cfg.auth.switch_account_url;
- } else {
- this._switchAccountUrl = '';
- }
- this._hasAvatars = !!(cfg && cfg.plugin && cfg.plugin.has_avatars);
- });
- }
-
- /** @override */
- detached() {
- super.detached();
- this.unlisten(window, 'location-change', '_handleLocationChange');
- }
-
- _getLinks(switchAccountUrl, path) {
- // Polymer 2: check for undefined
- if ([switchAccountUrl, path].includes(undefined)) {
- return undefined;
- }
-
- const links = [];
- links.push({name: 'Settings', url: '/settings/'});
- links.push({name: 'Keyboard Shortcuts', id: 'shortcuts'});
- if (switchAccountUrl) {
- const replacements = {path};
- const url = this._interpolateUrl(switchAccountUrl, replacements);
- links.push({name: 'Switch account', url, external: true});
- }
- links.push({name: 'Sign out', url: '/logout'});
- return links;
- }
-
- _getTopContent(account) {
- return [
- {text: this._accountName(account), bold: true},
- {text: account.email ? account.email : ''},
- ];
- }
-
- _handleShortcutsTap(e) {
- this.dispatchEvent(new CustomEvent('show-keyboard-shortcuts',
- {bubbles: true, composed: true}));
- }
-
- _handleLocationChange() {
- this._path =
- window.location.pathname +
- window.location.search +
- window.location.hash;
- }
-
- _interpolateUrl(url, replacements) {
- return url.replace(
- INTERPOLATE_URL_PATTERN,
- (match, p1) => replacements[p1] || '');
- }
-
- _accountName(account) {
- return getUserName(this.config, account);
- }
-}
-
-customElements.define(GrAccountDropdown.is, GrAccountDropdown);
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
new file mode 100644
index 0000000..ef0ced8
--- /dev/null
+++ b/polygerrit-ui/app/elements/core/gr-account-dropdown/gr-account-dropdown.ts
@@ -0,0 +1,146 @@
+/**
+ * @license
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import '../../shared/gr-button/gr-button';
+import '../../shared/gr-dropdown/gr-dropdown';
+import '../../shared/gr-rest-api-interface/gr-rest-api-interface';
+import '../../../styles/shared-styles';
+import '../../shared/gr-avatar/gr-avatar';
+import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners';
+import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin';
+import {PolymerElement} from '@polymer/polymer/polymer-element';
+import {htmlTemplate} from './gr-account-dropdown_html';
+import {getUserName} from '../../../utils/display-name-util';
+import {customElement, property} from '@polymer/decorators';
+import {AccountInfo, ServerInfo} from '../../../types/common';
+import {RestApiService} from '../../../services/services/gr-rest-api/gr-rest-api';
+
+const INTERPOLATE_URL_PATTERN = /\${([\w]+)}/g;
+
+declare global {
+ interface HTMLElementTagNameMap {
+ 'gr-account-dropdown': GrAccountDropdown;
+ }
+}
+
+export interface GrAccountDropdown {
+ $: {
+ restAPI: RestApiService & Element;
+ };
+}
+
+@customElement('gr-account-dropdown')
+export class GrAccountDropdown extends GestureEventListeners(
+ LegacyElementMixin(PolymerElement)
+) {
+ static get template() {
+ return htmlTemplate;
+ }
+
+ @property({type: Object})
+ account?: AccountInfo;
+
+ @property({type: Object})
+ config?: ServerInfo;
+
+ @property({type: Array, computed: '_getLinks(_switchAccountUrl, _path)'})
+ links?: string[];
+
+ @property({type: Array, computed: '_getTopContent(account)'})
+ topContent?: string[];
+
+ @property({type: String})
+ _path = '/';
+
+ @property({type: Boolean})
+ _hasAvatars = false;
+
+ @property({type: String})
+ _switchAccountUrl = '';
+
+ /** @override */
+ attached() {
+ super.attached();
+ this._handleLocationChange();
+ this.listen(window, 'location-change', '_handleLocationChange');
+ this.$.restAPI.getConfig().then(cfg => {
+ this.config = cfg;
+
+ if (cfg && cfg.auth && cfg.auth.switch_account_url) {
+ this._switchAccountUrl = cfg.auth.switch_account_url;
+ } else {
+ this._switchAccountUrl = '';
+ }
+ this._hasAvatars = !!(cfg && cfg.plugin && cfg.plugin.has_avatars);
+ });
+ }
+
+ /** @override */
+ detached() {
+ super.detached();
+ this.unlisten(window, 'location-change', '_handleLocationChange');
+ }
+
+ _getLinks(switchAccountUrl: string, path: string) {
+ // Polymer 2: check for undefined
+ if (switchAccountUrl === undefined || path === undefined) {
+ return undefined;
+ }
+
+ const links = [];
+ links.push({name: 'Settings', url: '/settings/'});
+ links.push({name: 'Keyboard Shortcuts', id: 'shortcuts'});
+ if (switchAccountUrl) {
+ const replacements = {path};
+ const url = this._interpolateUrl(switchAccountUrl, replacements);
+ links.push({name: 'Switch account', url, external: true});
+ }
+ links.push({name: 'Sign out', url: '/logout'});
+ return links;
+ }
+
+ _getTopContent(account?: AccountInfo) {
+ return [
+ {text: this._accountName(account), bold: true},
+ {text: account?.email ? account.email : ''},
+ ];
+ }
+
+ _handleShortcutsTap() {
+ this.dispatchEvent(
+ new CustomEvent('show-keyboard-shortcuts', {
+ bubbles: true,
+ composed: true,
+ })
+ );
+ }
+
+ _handleLocationChange() {
+ this._path =
+ window.location.pathname + window.location.search + window.location.hash;
+ }
+
+ _interpolateUrl(url: string, replacements: {[key: string]: string}) {
+ return url.replace(
+ INTERPOLATE_URL_PATTERN,
+ (_, p1) => replacements[p1] || ''
+ );
+ }
+
+ _accountName(account?: AccountInfo) {
+ return getUserName(this.config, account);
+ }
+}
diff --git a/polygerrit-ui/app/elements/diff/gr-comment-api/gr-comment-api.ts b/polygerrit-ui/app/elements/diff/gr-comment-api/gr-comment-api.ts
index a2036b7..b49f522 100644
--- a/polygerrit-ui/app/elements/diff/gr-comment-api/gr-comment-api.ts
+++ b/polygerrit-ui/app/elements/diff/gr-comment-api/gr-comment-api.ts
@@ -541,6 +541,9 @@
comments: T[]
): T[] {
return comments.slice(0).sort((c1, c2) => {
+ const d1 = !!(c1 as HumanCommentInfoWithPath).__draft;
+ const d2 = !!(c2 as HumanCommentInfoWithPath).__draft;
+ if (d1 !== d2) return d1 ? 1 : -1;
const dateDiff =
parseDate(c1.updated).valueOf() - parseDate(c2.updated).valueOf();
if (dateDiff) {
diff --git a/polygerrit-ui/app/elements/diff/gr-comment-api/gr-comment-api_test.js b/polygerrit-ui/app/elements/diff/gr-comment-api/gr-comment-api_test.js
index 0a7c3b5..df5e450 100644
--- a/polygerrit-ui/app/elements/diff/gr-comment-api/gr-comment-api_test.js
+++ b/polygerrit-ui/app/elements/diff/gr-comment-api/gr-comment-api_test.js
@@ -227,10 +227,12 @@
},
{
id: 12,
- in_reply_to: 2,
+ in_reply_to: 4,
patch_set: 2,
line: 1,
- updated: makeTime(3),
+ // Draft gets lower timestamp than published comment, because we
+ // want to test that the draft still gets sorted to the end.
+ updated: makeTime(2),
},
],
'file/two': [
@@ -262,7 +264,7 @@
patch_set: 2,
unresolved: true,
line: 1,
- updated: makeTime(2),
+ updated: makeTime(3),
},
],
};
@@ -549,16 +551,16 @@
unresolved: true,
line: 1,
__path: 'file/one',
- updated: '2013-02-26 15:02:43.986000000',
+ updated: '2013-02-26 15:03:43.986000000',
},
{
id: 12,
- in_reply_to: 2,
+ in_reply_to: 4,
patch_set: 2,
line: 1,
__path: 'file/one',
__draft: true,
- updated: '2013-02-26 15:03:43.986000000',
+ updated: '2013-02-26 15:02:43.986000000',
},
],
patchNum: 2,
@@ -708,16 +710,16 @@
patch_set: 2,
unresolved: true,
line: 1,
- updated: '2013-02-26 15:02:43.986000000',
+ updated: '2013-02-26 15:03:43.986000000',
},
{
__path: 'file/one',
__draft: true,
id: 12,
- in_reply_to: 2,
+ in_reply_to: 4,
patch_set: 2,
line: 1,
- updated: '2013-02-26 15:03:43.986000000',
+ updated: '2013-02-26 15:02:43.986000000',
},
];
assert.deepEqual(element._changeComments.getCommentsForThread(4),
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host.js b/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host.js
index 5a36917..7693d56 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host.js
@@ -331,7 +331,7 @@
const layers = [this.$.syntaxLayer];
// Get layers from plugins (if any).
for (const pluginLayer of this.$.jsAPI.getDiffLayers(
- this.path, this.changeNum, this.patchNum)) {
+ this.path, this.changeNum)) {
layers.push(pluginLayer);
}
this._layers = layers;
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host_test.js b/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host_test.js
index 37d8819..cd43fdb 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host_test.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host_test.js
@@ -979,12 +979,6 @@
assert.equal(element.$.diff.displayLine, value);
});
- test('passes in commitRange', () => {
- const value = {};
- element.commitRange = value;
- assert.equal(element.$.diff.commitRange, value);
- });
-
test('passes in hidden', () => {
const value = true;
element.hidden = value;
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.js b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.js
index 0b02245..ab706e9 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.js
@@ -314,12 +314,14 @@
this.addEventListener('open-fix-preview',
e => this._onOpenFixPreview(e));
this.$.cursor.push('diffs', this.$.diffHost);
-
- const onRender = () => {
- this.$.diffHost.removeEventListener('render', onRender);
+ this._onRenderHandler = () => {
this.$.cursor.reInitCursor();
};
- this.$.diffHost.addEventListener('render', onRender);
+ this.$.diffHost.addEventListener('render', this._onRenderHandler);
+ }
+
+ detached() {
+ this.$.diffHost.removeEventListener('render', this._onRenderHandler);
}
_getLoggedIn() {
diff --git a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.ts b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.ts
index 1c52b5f..08414ae 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.ts
+++ b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.ts
@@ -347,7 +347,7 @@
// document.getSelection() cannot reference the actual DOM elements making
// up the diff, because they are in the shadow DOM of the gr-diff element.
// This takes the shadow DOM selection if one exists.
- return this.root instanceof ShadowRoot
+ return this.root instanceof ShadowRoot && this.root.getSelection
? this.root.getSelection()
: document.getSelection();
}
@@ -361,28 +361,31 @@
});
}
+ // TODO(brohlfs): Rewrite gr-diff to be agnostic of GrCommentThread, because
+ // other users of gr-diff may use different comment widgets.
_updateRanges(
addedThreadEls: GrCommentThread[],
removedThreadEls: GrCommentThread[]
) {
function commentRangeFromThreadEl(
threadEl: GrCommentThread
- ): CommentRangeLayer {
+ ): CommentRangeLayer | undefined {
const side = getSide(threadEl);
const rangeAtt = threadEl.getAttribute('range');
- if (!rangeAtt) throw Error('comment thread without range');
+ if (!rangeAtt) return undefined;
const range = JSON.parse(rangeAtt) as CommentRange;
return {side, range, hovering: false, rootId: threadEl.rootId};
}
+ // TODO(brohlfs): Rewrite `.map().filter() as ...` with `.reduce()` instead.
const addedCommentRanges = addedThreadEls
.map(commentRangeFromThreadEl)
- .filter(({range}) => range);
+ .filter(range => !!range) as CommentRangeLayer[];
const removedCommentRanges = removedThreadEls
.map(commentRangeFromThreadEl)
- .filter(({range}) => range);
+ .filter(range => !!range) as CommentRangeLayer[];
for (const removedCommentRange of removedCommentRanges) {
const i = this._commentRanges.findIndex(
cr =>
diff --git a/polygerrit-ui/app/elements/gr-app-global-var-init.js b/polygerrit-ui/app/elements/gr-app-global-var-init.ts
similarity index 69%
rename from polygerrit-ui/app/elements/gr-app-global-var-init.js
rename to polygerrit-ui/app/elements/gr-app-global-var-init.ts
index 07d37fa..e609e6f 100644
--- a/polygerrit-ui/app/elements/gr-app-global-var-init.js
+++ b/polygerrit-ui/app/elements/gr-app-global-var-init.ts
@@ -22,53 +22,81 @@
* expose these variables until plugins switch to direct import from polygerrit.
*/
-import {getAccountDisplayName, getDisplayName, getGroupDisplayName, getUserName} from '../utils/display-name-util.js';
-import {GrAnnotation} from './diff/gr-diff-highlight/gr-annotation.js';
-import {GrAttributeHelper} from './plugins/gr-attribute-helper/gr-attribute-helper.js';
-import {GrDiffLine, GrDiffLineType} from './diff/gr-diff/gr-diff-line.js';
-import {GrDiffGroup, GrDiffGroupType} from './diff/gr-diff/gr-diff-group.js';
-import {GrDiffBuilder} from './diff/gr-diff-builder/gr-diff-builder.js';
-import {GrDiffBuilderSideBySide} from './diff/gr-diff-builder/gr-diff-builder-side-by-side.js';
-import {GrDiffBuilderImage} from './diff/gr-diff-builder/gr-diff-builder-image.js';
-import {GrDiffBuilderUnified} from './diff/gr-diff-builder/gr-diff-builder-unified.js';
-import {GrDiffBuilderBinary} from './diff/gr-diff-builder/gr-diff-builder-binary.js';
-import {GrChangeActionsInterface} from './shared/gr-js-api-interface/gr-change-actions-js-api.js';
-import {GrChangeReplyInterface} from './shared/gr-js-api-interface/gr-change-reply-js-api.js';
-import {GrEditConstants} from './edit/gr-edit-constants.js';
-import {GrDomHooksManager, GrDomHook} from './plugins/gr-dom-hooks/gr-dom-hooks.js';
-import {GrEtagDecorator} from './shared/gr-rest-api-interface/gr-etag-decorator.js';
-import {GrThemeApi} from './plugins/gr-theme-api/gr-theme-api.js';
-import {SiteBasedCache, FetchPromisesCache, GrRestApiHelper} from './shared/gr-rest-api-interface/gr-rest-apis/gr-rest-api-helper.js';
-import {GrLinkTextParser} from './shared/gr-linked-text/link-text-parser.js';
-import {getPluginEndpoints, GrPluginEndpoints} from './shared/gr-js-api-interface/gr-plugin-endpoints.js';
-import {GrReviewerUpdatesParser} from './shared/gr-rest-api-interface/gr-reviewer-updates-parser.js';
-import {GrPopupInterface} from './plugins/gr-popup-interface/gr-popup-interface.js';
-import {GrCountStringFormatter} from './shared/gr-count-string-formatter/gr-count-string-formatter.js';
-import {GrReviewerSuggestionsProvider, SUGGESTIONS_PROVIDERS_USERS_TYPES} from '../scripts/gr-reviewer-suggestions-provider/gr-reviewer-suggestions-provider.js';
-import {util} from '../scripts/util.js';
-import {page} from '../utils/page-wrapper-utils.js';
-import {appContext} from '../services/app-context.js';
-import {GrAdminApi} from './plugins/gr-admin-api/gr-admin-api.js';
-import {GrAnnotationActionsContext} from './shared/gr-js-api-interface/gr-annotation-actions-context.js';
-import {GrAnnotationActionsInterface} from './shared/gr-js-api-interface/gr-annotation-actions-js-api.js';
-import {GrChangeMetadataApi} from './plugins/gr-change-metadata-api/gr-change-metadata-api.js';
-import {GrEmailSuggestionsProvider} from '../scripts/gr-email-suggestions-provider/gr-email-suggestions-provider.js';
-import {GrGroupSuggestionsProvider} from '../scripts/gr-group-suggestions-provider/gr-group-suggestions-provider.js';
-import {GrEventHelper} from './plugins/gr-event-helper/gr-event-helper.js';
-import {GrPluginRestApi} from './shared/gr-js-api-interface/gr-plugin-rest-api.js';
-import {GrRepoApi} from './plugins/gr-repo-api/gr-repo-api.js';
-import {GrSettingsApi} from './plugins/gr-settings-api/gr-settings-api.js';
-import {GrStylesApi} from './plugins/gr-styles-api/gr-styles-api.js';
-import {getPluginLoader, PluginLoader} from './shared/gr-js-api-interface/gr-plugin-loader.js';
-import {GrPluginActionContext} from './shared/gr-js-api-interface/gr-plugin-action-context.js';
-import {getPluginNameFromUrl, getRestAPI, PLUGIN_LOADING_TIMEOUT_MS, PRELOADED_PROTOCOL, send} from './shared/gr-js-api-interface/gr-api-utils.js';
-import {getBaseUrl} from '../utils/url-util.js';
-import {GerritNav} from './core/gr-navigation/gr-navigation.js';
-import {getRootElement} from '../scripts/rootElement.js';
-import {rangesEqual} from './diff/gr-diff/gr-diff-utils.js';
-import {RevisionInfo} from './shared/revision-info/revision-info.js';
-import {CoverageType} from '../types/types.js';
-import {_setHiddenScroll, getHiddenScroll} from '../scripts/hiddenscroll.js';
+import {
+ getAccountDisplayName,
+ getDisplayName,
+ getGroupDisplayName,
+ getUserName,
+} from '../utils/display-name-util';
+import {GrAnnotation} from './diff/gr-diff-highlight/gr-annotation';
+import {GrAttributeHelper} from './plugins/gr-attribute-helper/gr-attribute-helper';
+import {GrDiffLine, GrDiffLineType} from './diff/gr-diff/gr-diff-line';
+import {GrDiffGroup, GrDiffGroupType} from './diff/gr-diff/gr-diff-group';
+import {GrDiffBuilder} from './diff/gr-diff-builder/gr-diff-builder';
+import {GrDiffBuilderSideBySide} from './diff/gr-diff-builder/gr-diff-builder-side-by-side';
+import {GrDiffBuilderImage} from './diff/gr-diff-builder/gr-diff-builder-image';
+import {GrDiffBuilderUnified} from './diff/gr-diff-builder/gr-diff-builder-unified';
+import {GrDiffBuilderBinary} from './diff/gr-diff-builder/gr-diff-builder-binary';
+import {GrChangeActionsInterface} from './shared/gr-js-api-interface/gr-change-actions-js-api';
+import {GrChangeReplyInterface} from './shared/gr-js-api-interface/gr-change-reply-js-api';
+import {GrEditConstants} from './edit/gr-edit-constants';
+import {
+ GrDomHooksManager,
+ GrDomHook,
+} from './plugins/gr-dom-hooks/gr-dom-hooks';
+import {GrEtagDecorator} from './shared/gr-rest-api-interface/gr-etag-decorator';
+import {GrThemeApi} from './plugins/gr-theme-api/gr-theme-api';
+import {
+ SiteBasedCache,
+ FetchPromisesCache,
+ GrRestApiHelper,
+} from './shared/gr-rest-api-interface/gr-rest-apis/gr-rest-api-helper';
+import {GrLinkTextParser} from './shared/gr-linked-text/link-text-parser';
+import {
+ getPluginEndpoints,
+ GrPluginEndpoints,
+} from './shared/gr-js-api-interface/gr-plugin-endpoints';
+import {GrReviewerUpdatesParser} from './shared/gr-rest-api-interface/gr-reviewer-updates-parser';
+import {GrPopupInterface} from './plugins/gr-popup-interface/gr-popup-interface';
+import {GrCountStringFormatter} from './shared/gr-count-string-formatter/gr-count-string-formatter';
+import {
+ GrReviewerSuggestionsProvider,
+ SUGGESTIONS_PROVIDERS_USERS_TYPES,
+} from '../scripts/gr-reviewer-suggestions-provider/gr-reviewer-suggestions-provider';
+import {util} from '../scripts/util';
+import {page} from '../utils/page-wrapper-utils';
+import {appContext} from '../services/app-context';
+import {GrAdminApi} from './plugins/gr-admin-api/gr-admin-api';
+import {GrAnnotationActionsContext} from './shared/gr-js-api-interface/gr-annotation-actions-context';
+import {GrAnnotationActionsInterface} from './shared/gr-js-api-interface/gr-annotation-actions-js-api';
+import {GrChangeMetadataApi} from './plugins/gr-change-metadata-api/gr-change-metadata-api';
+import {GrEmailSuggestionsProvider} from '../scripts/gr-email-suggestions-provider/gr-email-suggestions-provider';
+import {GrGroupSuggestionsProvider} from '../scripts/gr-group-suggestions-provider/gr-group-suggestions-provider';
+import {GrEventHelper} from './plugins/gr-event-helper/gr-event-helper';
+import {GrPluginRestApi} from './shared/gr-js-api-interface/gr-plugin-rest-api';
+import {GrRepoApi} from './plugins/gr-repo-api/gr-repo-api';
+import {GrSettingsApi} from './plugins/gr-settings-api/gr-settings-api';
+import {GrStylesApi} from './plugins/gr-styles-api/gr-styles-api';
+import {
+ getPluginLoader,
+ PluginLoader,
+} from './shared/gr-js-api-interface/gr-plugin-loader';
+import {GrPluginActionContext} from './shared/gr-js-api-interface/gr-plugin-action-context';
+import {
+ getPluginNameFromUrl,
+ getRestAPI,
+ PLUGIN_LOADING_TIMEOUT_MS,
+ PRELOADED_PROTOCOL,
+ send,
+} from './shared/gr-js-api-interface/gr-api-utils';
+import {getBaseUrl} from '../utils/url-util';
+import {GerritNav} from './core/gr-navigation/gr-navigation';
+import {getRootElement} from '../scripts/rootElement';
+import {rangesEqual} from './diff/gr-diff/gr-diff-utils';
+import {RevisionInfo} from './shared/revision-info/revision-info';
+import {CoverageType} from '../types/types';
+import {_setHiddenScroll, getHiddenScroll} from '../scripts/hiddenscroll';
+import {GerritGlobal} from './shared/gr-js-api-interface/gr-gerrit';
export function initGlobalVariables() {
window.GrDisplayNameUtils = {
@@ -131,7 +159,7 @@
PLUGIN_LOADING_TIMEOUT_MS,
};
- window.Gerrit = window.Gerrit || {};
+ window.Gerrit = (window.Gerrit || {}) as GerritGlobal;
window.Gerrit.Nav = GerritNav;
window.Gerrit.getRootElement = getRootElement;
window.Gerrit.Auth = appContext.authService;
@@ -140,10 +168,10 @@
// TODO: should define as a getter
window.Gerrit._endpoints = getPluginEndpoints();
- window.Gerrit.slotToContent = slot => slot;
+ // TODO(TS): seems not used, probably just remove
+ window.Gerrit.slotToContent = (slot: any) => slot;
window.Gerrit.rangesEqual = rangesEqual;
- window.Gerrit.SUGGESTIONS_PROVIDERS_USERS_TYPES =
- SUGGESTIONS_PROVIDERS_USERS_TYPES;
+ window.Gerrit.SUGGESTIONS_PROVIDERS_USERS_TYPES = SUGGESTIONS_PROVIDERS_USERS_TYPES;
window.Gerrit.RevisionInfo = RevisionInfo;
window.Gerrit.CoverageType = CoverageType;
Object.defineProperty(window.Gerrit, 'hiddenscroll', {
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 e4cf3a1..d970b0a 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
@@ -164,25 +164,30 @@
}
_maybeSetName() {
+ // Note that we are intentionally not acting on this._account.name being the
+ // empty string (which is falsy).
return this._hasNameChange && this.nameMutable && this._account?.name
? this.$.restAPI.setAccountName(this._account.name)
: Promise.resolve();
}
_maybeSetUsername() {
+ // Note that we are intentionally not acting on this._username being the
+ // empty string (which is falsy).
return this._hasUsernameChange && this.usernameMutable && this._username
? this.$.restAPI.setAccountUsername(this._username)
: Promise.resolve();
}
_maybeSetDisplayName() {
- return this._hasDisplayNameChange && this._account?.display_name
+ return this._hasDisplayNameChange &&
+ this._account?.display_name !== undefined
? this.$.restAPI.setAccountDisplayName(this._account.display_name)
: Promise.resolve();
}
_maybeSetStatus() {
- return this._hasStatusChange && this._account?.status
+ return this._hasStatusChange && this._account?.status !== undefined
? this.$.restAPI.setAccountStatus(this._account.status)
: Promise.resolve();
}
diff --git a/polygerrit-ui/app/elements/shared/gr-account-list/gr-account-list.js b/polygerrit-ui/app/elements/shared/gr-account-list/gr-account-list.js
deleted file mode 100644
index 76899b3..0000000
--- a/polygerrit-ui/app/elements/shared/gr-account-list/gr-account-list.js
+++ /dev/null
@@ -1,369 +0,0 @@
-/**
- * @license
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-import '../gr-account-chip/gr-account-chip.js';
-import '../gr-account-entry/gr-account-entry.js';
-import '../../../styles/shared-styles.js';
-import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js';
-import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
-import {PolymerElement} from '@polymer/polymer/polymer-element.js';
-import {htmlTemplate} from './gr-account-list_html.js';
-import {appContext} from '../../../services/app-context.js';
-
-const VALID_EMAIL_ALERT = 'Please input a valid email.';
-
-/**
- * @extends PolymerElement
- */
-class GrAccountList extends GestureEventListeners(
- LegacyElementMixin(PolymerElement)) {
- static get template() { return htmlTemplate; }
-
- static get is() { return 'gr-account-list'; }
- /**
- * Fired when user inputs an invalid email address.
- *
- * @event show-alert
- */
-
- static get properties() {
- return {
- accounts: {
- type: Array,
- value() { return []; },
- notify: true,
- },
- change: Object,
- filter: Function,
- placeholder: String,
- disabled: {
- type: Function,
- value: false,
- },
-
- /**
- * Returns suggestions and convert them to list item
- *
- * @type {Gerrit.GrSuggestionsProvider}
- */
- suggestionsProvider: {
- type: Object,
- },
-
- /**
- * Needed for template checking since value is initially set to null.
- *
- * @type {?Object}
- */
- pendingConfirmation: {
- type: Object,
- value: null,
- notify: true,
- },
- readonly: {
- type: Boolean,
- value: false,
- },
- /**
- * When true, allows for non-suggested inputs to be added.
- */
- allowAnyInput: {
- type: Boolean,
- value: false,
- },
-
- /**
- * Array of values (groups/accounts) that are removable. When this prop is
- * undefined, all values are removable.
- */
- removableValues: Array,
- maxCount: {
- type: Number,
- value: 0,
- },
-
- /**
- * Returns suggestion items
- *
- * @type {!function(string): Promise<Array<Gerrit.GrSuggestionItem>>}
- */
- _querySuggestions: {
- type: Function,
- value() {
- return input => this._getSuggestions(input);
- },
- },
-
- /**
- * Set to true to disable suggestions on empty input.
- */
- skipSuggestOnEmpty: {
- type: Boolean,
- value: false,
- },
- };
- }
-
- constructor() {
- super();
- this.reporting = appContext.reportingService;
- }
-
- /** @override */
- created() {
- super.created();
- this.addEventListener('remove',
- e => this._handleRemove(e));
- }
-
- get accountChips() {
- return Array.from(
- this.root.querySelectorAll('gr-account-chip'));
- }
-
- get focusStart() {
- return this.$.entry.focusStart;
- }
-
- _getSuggestions(input) {
- if (this.skipSuggestOnEmpty && !input) {
- return Promise.resolve([]);
- }
- const provider = this.suggestionsProvider;
- if (!provider) {
- return Promise.resolve([]);
- }
- return provider.getSuggestions(input).then(suggestions => {
- if (!suggestions) { return []; }
- if (this.filter) {
- suggestions = suggestions.filter(this.filter);
- }
- return suggestions.map(suggestion =>
- provider.makeSuggestionItem(suggestion));
- });
- }
-
- _handleAdd(e) {
- this.addAccountItem(e.detail.value);
- }
-
- addAccountItem(item) {
- // 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 itemTypeAdded = 'unknown';
- if (item.account) {
- const account =
- {...item.account, _pendingAdd: true};
- this.push('accounts', account);
- itemTypeAdded = 'account';
- } else if (item.group) {
- if (item.confirm) {
- this.pendingConfirmation = item;
- return;
- }
- const group = {...item.group,
- _pendingAdd: true, _group: true};
- this.push('accounts', group);
- itemTypeAdded = 'group';
- } else if (this.allowAnyInput) {
- if (!item.includes('@')) {
- // Repopulate the input with what the user tried to enter and have
- // a toast tell them why they can't enter it.
- this.$.entry.setText(item);
- this.dispatchEvent(new CustomEvent('show-alert', {
- detail: {message: VALID_EMAIL_ALERT},
- bubbles: true,
- composed: true,
- }));
- return false;
- } else {
- const account = {email: item, _pendingAdd: true};
- this.push('accounts', account);
- itemTypeAdded = 'email';
- }
- }
-
- this.reporting.reportInteraction(`Add to ${this.id}`, {itemTypeAdded});
- this.pendingConfirmation = null;
- return true;
- }
-
- confirmGroup(group) {
- group = {
- ...group, confirmed: true, _pendingAdd: true, _group: true};
- this.push('accounts', group);
- this.pendingConfirmation = null;
- }
-
- _computeChipClass(account) {
- const classes = [];
- if (account._group) {
- classes.push('group');
- }
- if (account._pendingAdd) {
- classes.push('pendingAdd');
- }
- return classes.join(' ');
- }
-
- _accountMatches(a, b) {
- if (a && b) {
- if (a._account_id) {
- return a._account_id === b._account_id;
- }
- if (a.email) {
- return a.email === b.email;
- }
- }
- return a === b;
- }
-
- _computeRemovable(account, readonly) {
- if (readonly) { return false; }
- if (this.removableValues) {
- for (let i = 0; i < this.removableValues.length; i++) {
- if (this._accountMatches(this.removableValues[i], account)) {
- return true;
- }
- }
- return !!account._pendingAdd;
- }
- return true;
- }
-
- _handleRemove(e) {
- const toRemove = e.detail.account;
- this.removeAccount(toRemove);
- this.$.entry.focus();
- }
-
- removeAccount(toRemove) {
- if (!toRemove || !this._computeRemovable(toRemove, this.readonly)) {
- return;
- }
- for (let i = 0; i < this.accounts.length; i++) {
- let matches;
- const account = this.accounts[i];
- if (toRemove._group) {
- matches = toRemove.id === account.id;
- } else {
- matches = this._accountMatches(toRemove, account);
- }
- if (matches) {
- this.splice('accounts', i, 1);
- this.reporting.reportInteraction(`Remove from ${this.id}`);
- return;
- }
- }
- console.warn('received remove event for missing account', toRemove);
- }
-
- _getNativeInput(paperInput) {
- // In Polymer 2 inputElement isn't nativeInput anymore
- return paperInput.$.nativeInput || paperInput.inputElement;
- }
-
- _handleInputKeydown(e) {
- const input = this._getNativeInput(e.detail.input);
- if (input.selectionStart !== input.selectionEnd ||
- input.selectionStart !== 0) {
- return;
- }
- switch (e.detail.keyCode) {
- case 8: // Backspace
- this.removeAccount(this.accounts[this.accounts.length - 1]);
- break;
- case 37: // Left arrow
- if (this.accountChips[this.accountChips.length - 1]) {
- this.accountChips[this.accountChips.length - 1].focus();
- }
- break;
- }
- }
-
- _handleChipKeydown(e) {
- const chip = e.target;
- const chips = this.accountChips;
- const index = chips.indexOf(chip);
- switch (e.keyCode) {
- case 8: // Backspace
- case 13: // Enter
- case 32: // Spacebar
- case 46: // Delete
- this.removeAccount(chip.account);
- // Splice from this array to avoid inconsistent ordering of
- // event handling.
- chips.splice(index, 1);
- if (index < chips.length) {
- chips[index].focus();
- } else if (index > 0) {
- chips[index - 1].focus();
- } else {
- this.$.entry.focus();
- }
- break;
- case 37: // Left arrow
- if (index > 0) {
- chip.blur();
- chips[index - 1].focus();
- }
- break;
- case 39: // Right arrow
- chip.blur();
- if (index < chips.length - 1) {
- chips[index + 1].focus();
- } else {
- this.$.entry.focus();
- }
- break;
- }
- }
-
- /**
- * Submit the text of the entry as a reviewer value, if it exists. If it is
- * a successful submit of the text, clear the entry value.
- *
- * @return {boolean} If there is text in the entry, return true if the
- * submission was successful and false if not. If there is no text,
- * return true.
- */
- submitEntryText() {
- const text = this.$.entry.getText();
- if (!text.length) { return true; }
- const wasSubmitted = this.addAccountItem(text);
- if (wasSubmitted) { this.$.entry.clear(); }
- return wasSubmitted;
- }
-
- additions() {
- return this.accounts
- .filter(account => account._pendingAdd)
- .map(account => {
- if (account._group) {
- return {group: account};
- } else {
- return {account};
- }
- });
- }
-
- _computeEntryHidden(maxCount, accountsRecord, readonly) {
- return (maxCount && maxCount <= accountsRecord.base.length) || readonly;
- }
-}
-
-customElements.define(GrAccountList.is, GrAccountList);
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
new file mode 100644
index 0000000..fc0ea79
--- /dev/null
+++ b/polygerrit-ui/app/elements/shared/gr-account-list/gr-account-list.ts
@@ -0,0 +1,452 @@
+/**
+ * @license
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import '../gr-account-chip/gr-account-chip';
+import '../gr-account-entry/gr-account-entry';
+import '../../../styles/shared-styles';
+import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners';
+import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin';
+import {PolymerElement} from '@polymer/polymer/polymer-element';
+import {htmlTemplate} from './gr-account-list_html';
+import {appContext} from '../../../services/app-context';
+import {customElement, property} from '@polymer/decorators';
+import {
+ ChangeInfo,
+ Suggestion,
+ AccountInfo,
+ GroupInfo,
+} from '../../../types/common';
+import {
+ GrReviewerSuggestionsProvider,
+ SuggestionItem,
+} from '../../../scripts/gr-reviewer-suggestions-provider/gr-reviewer-suggestions-provider';
+import {ReportingService} from '../../../services/gr-reporting/gr-reporting';
+import {GrAccountEntry} from '../gr-account-entry/gr-account-entry';
+import {GrAccountChip} from '../gr-account-chip/gr-account-chip';
+import {PolymerDeepPropertyChange} from '@polymer/polymer/interfaces';
+import {PaperInputElementExt} from '../../../types/types';
+
+const VALID_EMAIL_ALERT = 'Please input a valid email.';
+
+declare global {
+ interface HTMLElementTagNameMap {
+ 'gr-account-list': GrAccountList;
+ }
+}
+
+export interface GrAccountList {
+ $: {
+ entry: GrAccountEntry;
+ };
+}
+
+/**
+ * For item added with account info
+ */
+export interface AccountObjectInput {
+ account: AccountInfo;
+}
+
+/**
+ * For item added with group info
+ */
+export interface GroupObjectInput {
+ group: GroupInfo;
+ confirm: boolean;
+}
+
+/** Supported input to be added */
+export type RawAccountInput = string | AccountObjectInput | GroupObjectInput;
+
+// type guards for AccountObjectInput and GroupObjectInput
+function isAccountObject(x: RawAccountInput): x is AccountObjectInput {
+ return !!(x as AccountObjectInput).account;
+}
+
+function isGroupObjectInput(x: RawAccountInput): x is GroupObjectInput {
+ return !!(x as GroupObjectInput).group;
+}
+
+// Internal input type with account info
+interface AccountInfoInput extends AccountInfo {
+ _group?: boolean;
+ _account?: boolean;
+ _pendingAdd?: boolean;
+ confirmed?: boolean;
+}
+
+// Internal input type with group info
+interface GroupInfoInput extends GroupInfo {
+ _group?: boolean;
+ _account?: boolean;
+ _pendingAdd?: boolean;
+ confirmed?: boolean;
+}
+
+type AccountInput = AccountInfoInput | GroupInfoInput;
+
+@customElement('gr-account-list')
+export class GrAccountList extends GestureEventListeners(
+ LegacyElementMixin(PolymerElement)
+) {
+ static get template() {
+ return htmlTemplate;
+ }
+
+ /**
+ * Fired when user inputs an invalid email address.
+ *
+ * @event show-alert
+ */
+
+ @property({type: Array, notify: true})
+ accounts: AccountInput[] = [];
+
+ @property({type: Object})
+ change?: ChangeInfo;
+
+ @property({type: Object})
+ filter?: (input: Suggestion) => boolean;
+
+ @property({type: String})
+ placeholder = '';
+
+ @property({type: Boolean})
+ disabled = false;
+
+ /**
+ * Returns suggestions and convert them to list item
+ */
+ @property({type: Object})
+ suggestionsProvider?: GrReviewerSuggestionsProvider;
+
+ /**
+ * Needed for template checking since value is initially set to null.
+ */
+ @property({type: Object, notify: true})
+ pendingConfirmation: RawAccountInput | null = null;
+
+ @property({type: Boolean})
+ readonly = false;
+
+ /**
+ * When true, allows for non-suggested inputs to be added.
+ */
+ @property({type: Boolean})
+ allowAnyInput = false;
+
+ /**
+ * Array of values (groups/accounts) that are removable. When this prop is
+ * undefined, all values are removable.
+ */
+ @property({type: Array})
+ removableValues?: AccountInput[];
+
+ @property({type: Number})
+ maxCount = 0;
+
+ /**
+ * Returns suggestion items
+ */
+ @property({type: Object})
+ _querySuggestions: (input: string) => Promise<SuggestionItem[]>;
+
+ /**
+ * Set to true to disable suggestions on empty input.
+ */
+ @property({type: Boolean})
+ skipSuggestOnEmpty = false;
+
+ reporting: ReportingService;
+
+ constructor() {
+ super();
+ this.reporting = appContext.reportingService;
+ this._querySuggestions = input => this._getSuggestions(input);
+ }
+
+ /** @override */
+ created() {
+ super.created();
+ this.addEventListener('remove', e =>
+ this._handleRemove(e as CustomEvent<{account: AccountInput}>)
+ );
+ }
+
+ get accountChips() {
+ return Array.from(this.root?.querySelectorAll('gr-account-chip') || []);
+ }
+
+ get focusStart() {
+ return this.$.entry.focusStart;
+ }
+
+ _getSuggestions(input: string) {
+ if (this.skipSuggestOnEmpty && !input) {
+ return Promise.resolve([]);
+ }
+ const provider = this.suggestionsProvider;
+ if (!provider) {
+ return Promise.resolve([]);
+ }
+ return provider.getSuggestions(input).then(suggestions => {
+ if (!suggestions) {
+ return [];
+ }
+ if (this.filter) {
+ suggestions = suggestions.filter(this.filter);
+ }
+ return suggestions.map(suggestion =>
+ provider.makeSuggestionItem(suggestion)
+ );
+ });
+ }
+
+ _handleAdd(e: CustomEvent<{value: RawAccountInput}>) {
+ this.addAccountItem(e.detail.value);
+ }
+
+ addAccountItem(item: RawAccountInput) {
+ // 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 itemTypeAdded = 'unknown';
+ if (isAccountObject(item)) {
+ const account = {...item.account, _pendingAdd: true};
+ this.push('accounts', account);
+ itemTypeAdded = 'account';
+ } else if (isGroupObjectInput(item)) {
+ if (item.confirm) {
+ this.pendingConfirmation = item;
+ return;
+ }
+ const group = {...item.group, _pendingAdd: true, _group: true};
+ this.push('accounts', group);
+ itemTypeAdded = 'group';
+ } else if (this.allowAnyInput) {
+ if (!item.includes('@')) {
+ // Repopulate the input with what the user tried to enter and have
+ // a toast tell them why they can't enter it.
+ this.$.entry.setText(item);
+ this.dispatchEvent(
+ new CustomEvent('show-alert', {
+ detail: {message: VALID_EMAIL_ALERT},
+ bubbles: true,
+ composed: true,
+ })
+ );
+ return false;
+ } else {
+ const account = {email: item, _pendingAdd: true};
+ this.push('accounts', account);
+ itemTypeAdded = 'email';
+ }
+ }
+
+ this.reporting.reportInteraction(`Add to ${this.id}`, {itemTypeAdded});
+ this.pendingConfirmation = null;
+ return true;
+ }
+
+ confirmGroup(group: GroupObjectInput) {
+ this.push('accounts', {
+ ...group,
+ confirmed: true,
+ _pendingAdd: true,
+ _group: true,
+ });
+ this.pendingConfirmation = null;
+ }
+
+ _computeChipClass(account: AccountInput) {
+ const classes = [];
+ if (account._group) {
+ classes.push('group');
+ }
+ if (account._pendingAdd) {
+ classes.push('pendingAdd');
+ }
+ return classes.join(' ');
+ }
+
+ _accountMatches(a: AccountInput, b: AccountInput) {
+ // TODO(TS): seems a & b always exists ?
+ if (a && b) {
+ // both conditions are checking against AccountInfo
+ // and only check a not b.. typeguard won't work very good without
+ // changing logic, so keep it as inline casting
+ if ((a as AccountInfoInput)._account_id) {
+ return (
+ (a as AccountInfoInput)._account_id ===
+ (b as AccountInfoInput)._account_id
+ );
+ }
+ if ((a as AccountInfoInput).email) {
+ return (a as AccountInfoInput).email === (b as AccountInfoInput).email;
+ }
+ }
+ return a === b;
+ }
+
+ _computeRemovable(account: AccountInput, readonly: boolean) {
+ if (readonly) {
+ return false;
+ }
+ if (this.removableValues) {
+ for (let i = 0; i < this.removableValues.length; i++) {
+ if (this._accountMatches(this.removableValues[i], account)) {
+ return true;
+ }
+ }
+ return !!account._pendingAdd;
+ }
+ return true;
+ }
+
+ _handleRemove(e: CustomEvent<{account: AccountInput}>) {
+ const toRemove = e.detail.account;
+ this.removeAccount(toRemove);
+ this.$.entry.focus();
+ }
+
+ removeAccount(toRemove?: AccountInput) {
+ if (!toRemove || !this._computeRemovable(toRemove, this.readonly)) {
+ return;
+ }
+ for (let i = 0; i < this.accounts.length; i++) {
+ let matches;
+ const account = this.accounts[i];
+ if (toRemove._group) {
+ matches =
+ (toRemove as GroupInfoInput).id === (account as GroupInfoInput).id;
+ } else {
+ matches = this._accountMatches(toRemove, account);
+ }
+ if (matches) {
+ this.splice('accounts', i, 1);
+ this.reporting.reportInteraction(`Remove from ${this.id}`);
+ return;
+ }
+ }
+ console.warn('received remove event for missing account', toRemove);
+ }
+
+ _getNativeInput(paperInput: PaperInputElementExt) {
+ // In Polymer 2 inputElement isn't nativeInput anymore
+ return (paperInput.$.nativeInput ||
+ paperInput.inputElement) as HTMLTextAreaElement;
+ }
+
+ _handleInputKeydown(
+ e: CustomEvent<{input: PaperInputElementExt; keyCode: number}>
+ ) {
+ const input = this._getNativeInput(e.detail.input);
+ if (
+ input.selectionStart !== input.selectionEnd ||
+ input.selectionStart !== 0
+ ) {
+ return;
+ }
+ switch (e.detail.keyCode) {
+ case 8: // Backspace
+ this.removeAccount(this.accounts[this.accounts.length - 1]);
+ break;
+ case 37: // Left arrow
+ if (this.accountChips[this.accountChips.length - 1]) {
+ this.accountChips[this.accountChips.length - 1].focus();
+ }
+ break;
+ }
+ }
+
+ _handleChipKeydown(e: KeyboardEvent) {
+ const chip = e.target as GrAccountChip;
+ const chips = this.accountChips;
+ const index = chips.indexOf(chip);
+ switch (e.keyCode) {
+ case 8: // Backspace
+ case 13: // Enter
+ case 32: // Spacebar
+ case 46: // Delete
+ this.removeAccount(chip.account);
+ // Splice from this array to avoid inconsistent ordering of
+ // event handling.
+ chips.splice(index, 1);
+ if (index < chips.length) {
+ chips[index].focus();
+ } else if (index > 0) {
+ chips[index - 1].focus();
+ } else {
+ this.$.entry.focus();
+ }
+ break;
+ case 37: // Left arrow
+ if (index > 0) {
+ chip.blur();
+ chips[index - 1].focus();
+ }
+ break;
+ case 39: // Right arrow
+ chip.blur();
+ if (index < chips.length - 1) {
+ chips[index + 1].focus();
+ } else {
+ this.$.entry.focus();
+ }
+ break;
+ }
+ }
+
+ /**
+ * Submit the text of the entry as a reviewer value, if it exists. If it is
+ * a successful submit of the text, clear the entry value.
+ *
+ * @return If there is text in the entry, return true if the
+ * submission was successful and false if not. If there is no text,
+ * return true.
+ */
+ submitEntryText() {
+ const text = this.$.entry.getText();
+ if (!text.length) {
+ return true;
+ }
+ const wasSubmitted = this.addAccountItem(text);
+ if (wasSubmitted) {
+ this.$.entry.clear();
+ }
+ return wasSubmitted;
+ }
+
+ additions() {
+ return this.accounts
+ .filter(account => account._pendingAdd)
+ .map(account => {
+ if (account._group) {
+ return {group: account};
+ } else {
+ return {account};
+ }
+ });
+ }
+
+ _computeEntryHidden(
+ maxCount: number,
+ accountsRecord: PolymerDeepPropertyChange<AccountInput[], AccountInput[]>,
+ readonly: boolean
+ ) {
+ return (maxCount && maxCount <= accountsRecord.base.length) || readonly;
+ }
+}
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 ea573ab..033b617 100644
--- a/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete.ts
+++ b/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete.ts
@@ -29,17 +29,17 @@
CustomKeyboardEvent,
} from '../../../mixins/keyboard-shortcut-mixin/keyboard-shortcut-mixin';
import {property, customElement, observe} from '@polymer/decorators';
-import {PaperInputElement} from '@polymer/paper-input/paper-input';
import {GrAutocompleteDropdown} from '../gr-autocomplete-dropdown/gr-autocomplete-dropdown';
import {GrCursorManager} from '../gr-cursor-manager/gr-cursor-manager';
import {EventWithPath} from '../../plugins/gr-event-helper/gr-event-helper';
+import {PaperInputElementExt} from '../../../types/types';
const TOKENIZE_REGEX = /(?:[^\s"]+|"[^"]*")+/g;
const DEBOUNCE_WAIT_MS = 200;
export interface GrAutocomplete {
$: {
- input: PaperInputElement & {$: {nativeInput?: Element}};
+ input: PaperInputElementExt;
suggestions: GrAutocompleteDropdown;
cursor: GrCursorManager;
};
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 0c896763..edadfba 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
@@ -29,7 +29,7 @@
import {htmlTemplate} from './gr-editable-label_html';
import {IronDropdownElement} from '@polymer/iron-dropdown/iron-dropdown';
import {dom, EventApi} from '@polymer/polymer/lib/legacy/polymer.dom';
-import {PaperInputElement} from '@polymer/paper-input/paper-input';
+import {PaperInputElementExt} from '../../../types/types';
const AWAIT_MAX_ITERS = 10;
const AWAIT_STEP = 5;
@@ -42,7 +42,7 @@
export interface GrEditableLabel {
$: {
- input: PaperInputElement;
+ input: PaperInputElementExt;
dropdown: IronDropdownElement;
};
}
@@ -188,12 +188,9 @@
}
get _nativeInput(): HTMLInputElement {
- // In Polymer 2, the namespace of nativeInput changed from input to
- // nativeInput.
- // `this.$.input` has type PaperInputElement, so this is beyond our control
- // and we cannot force `this.$.input.$` to have a proper type.
- return (this.$.input.$['nativeInput'] ||
- this.$.input.$['input']) as HTMLInputElement;
+ // In Polymer 2 inputElement isn't nativeInput anymore
+ return (this.$.input.$.nativeInput ||
+ this.$.input.inputElement) as HTMLInputElement;
}
_handleEnter(e: CustomKeyboardEvent) {
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-annotation-actions-context.ts b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-annotation-actions-context.ts
index 4a47a13..0cb628c 100644
--- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-annotation-actions-context.ts
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-annotation-actions-context.ts
@@ -17,7 +17,6 @@
import {GrAnnotation} from '../../diff/gr-diff-highlight/gr-annotation';
import {GrStyleObject} from '../../plugins/gr-styles-api/gr-styles-api';
-import {PatchSetNum} from '../../../types/common';
import {GrDiffLine} from '../../diff/gr-diff/gr-diff-line';
/**
@@ -43,15 +42,12 @@
changeNum: number;
- patchNum: number;
-
constructor(
contentEl: HTMLElement,
lineNumberEl: HTMLElement,
line: GrDiffLine,
path: string,
- changeNum: string | number,
- patchNum: PatchSetNum
+ changeNum: string | number
) {
this._contentEl = contentEl;
this._lineNumberEl = lineNumberEl;
@@ -59,9 +55,10 @@
this.line = line;
this.path = path;
this.changeNum = Number(changeNum);
- this.patchNum = Number(patchNum);
- if (isNaN(this.changeNum) || isNaN(this.patchNum)) {
- console.error('invalid parameters');
+ if (isNaN(this.changeNum)) {
+ console.error(
+ `GrAnnotationActionsContext: Invalid changeNum: ${changeNum}`
+ );
}
}
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-annotation-actions-js-api.ts b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-annotation-actions-js-api.ts
index 80e09d4..331fb42 100644
--- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-annotation-actions-js-api.ts
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-annotation-actions-js-api.ts
@@ -18,7 +18,6 @@
import {GrDiffLine, LineNumber} from '../../diff/gr-diff/gr-diff-line';
import {CoverageRange} from '../../../types/types';
import {Side} from '../../../constants/constants';
-import {PatchSetNum} from '../../../types/common';
import {PluginApi} from '../../plugins/gr-plugin-types';
type AddLayerFunc = (ctx: GrAnnotationActionsContext) => void;
@@ -188,13 +187,11 @@
*
* @param path The file path (eg: /COMMIT_MSG').
* @param changeNum The Gerrit change number.
- * @param patchNum The Gerrit patch number.
*/
- getLayer(path: string, changeNum: number, patchNum: number) {
+ getLayer(path: string, changeNum: number) {
const annotationLayer = new AnnotationLayer(
path,
changeNum,
- patchNum,
this.addLayerFunc
);
this.annotationLayers.push(annotationLayer);
@@ -216,14 +213,12 @@
*
* @param path The file path (eg: /COMMIT_MSG').
* @param changeNum The Gerrit change number.
- * @param patchNum The Gerrit patch number.
* @param addLayerFunc The function
* that will be called when the AnnotationLayer is ready to annotate.
*/
constructor(
readonly path: string,
private readonly changeNum: number,
- private readonly patchNum: number,
private readonly addLayerFunc: AddLayerFunc
) {
this.listeners = [];
@@ -264,8 +259,7 @@
lineNumberEl,
line,
this.path,
- this.changeNum,
- this.patchNum as PatchSetNum
+ this.changeNum
);
this.addLayerFunc(annotationActionsContext);
}
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-annotation-actions-js-api_test.js b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-annotation-actions-js-api_test.js
index e819529..8b3f501 100644
--- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-annotation-actions-js-api_test.js
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-annotation-actions-js-api_test.js
@@ -52,19 +52,17 @@
const el = document.createElement('div');
el.textContent = str;
const changeNum = 1234;
- const patchNum = 2;
let testLayerFuncCalled = false;
const testLayerFunc = context => {
testLayerFuncCalled = true;
assert.equal(context.line, line);
assert.equal(context.changeNum, changeNum);
- assert.equal(context.patchNum, 2);
};
annotationActions.addLayer(testLayerFunc);
const annotationLayer = annotationActions.getLayer(
- '/dummy/path', changeNum, patchNum);
+ '/dummy/path', changeNum);
const lineNumberEl = document.createElement('td');
annotationLayer.annotate(el, lineNumberEl, line);
@@ -74,8 +72,8 @@
test('add notifier', () => {
const path1 = '/dummy/path1';
const path2 = '/dummy/path2';
- const annotationLayer1 = annotationActions.getLayer(path1, 1, 2);
- const annotationLayer2 = annotationActions.getLayer(path2, 1, 2);
+ const annotationLayer1 = annotationActions.getLayer(path1, 1);
+ const annotationLayer2 = annotationActions.getLayer(path2, 1);
const layer1Spy = sinon.spy(annotationLayer1, 'notifyListeners');
const layer2Spy = sinon.spy(annotationLayer2, 'notifyListeners');
@@ -148,8 +146,7 @@
});
test('layer notify listeners', () => {
- const annotationLayer = annotationActions.getLayer(
- '/dummy/path', 1, 2);
+ const annotationLayer = annotationActions.getLayer('/dummy/path', 1);
let listenerCalledTimes = 0;
const startRange = 10;
const endRange = 20;
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-gerrit.ts b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-gerrit.ts
index 7f0f5b7..9e9a0ea 100644
--- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-gerrit.ts
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-gerrit.ts
@@ -19,7 +19,11 @@
* This defines the Gerrit instance. All methods directly attached to Gerrit
* should be defined or linked here.
*/
-import {getPluginLoader, PluginOptionMap} from './gr-plugin-loader';
+import {
+ getPluginLoader,
+ PluginOptionMap,
+ PluginLoader,
+} from './gr-plugin-loader';
import {getRestAPI, send} from './gr-api-utils';
import {appContext} from '../../../services/app-context';
import {PluginApi} from '../../plugins/gr-plugin-types';
@@ -29,8 +33,15 @@
EventCallback,
EventEmitterService,
} from '../../../services/gr-event-interface/gr-event-interface';
+import {GerritNav} from '../../core/gr-navigation/gr-navigation';
+import {getRootElement} from '../../../scripts/rootElement';
+import {GrPluginEndpoints} from './gr-plugin-endpoints';
+import {rangesEqual} from '../../diff/gr-diff/gr-diff-utils';
+import {SUGGESTIONS_PROVIDERS_USERS_TYPES} from '../../../scripts/gr-reviewer-suggestions-provider/gr-reviewer-suggestions-provider';
+import {CoverageType} from '../../../types/types';
+import {RevisionInfo} from '../revision-info/revision-info';
-interface GerritGlobal extends EventEmitterService {
+export interface GerritGlobal extends EventEmitterService {
flushPreinstalls?(): void;
css(rule: string): string;
install(
@@ -60,6 +71,18 @@
_isPluginLoaded(pathOrUrl: string): boolean;
_eventEmitter: EventEmitterService;
_customStyleSheet: CSSStyleSheet;
+
+ // exposed methods
+ Nav: typeof GerritNav;
+ Auth: typeof appContext.authService;
+ getRootElement: typeof getRootElement;
+ _pluginLoader: PluginLoader;
+ _endpoints: GrPluginEndpoints;
+ slotToContent(slot: unknown): unknown;
+ rangesEqual: typeof rangesEqual;
+ SUGGESTIONS_PROVIDERS_USERS_TYPES: typeof SUGGESTIONS_PROVIDERS_USERS_TYPES;
+ CoverageType: typeof CoverageType;
+ RevisionInfo: typeof RevisionInfo;
}
/**
@@ -67,7 +90,7 @@
* This needs to happen before Gerrit as plugin bundle overrides the Gerrit.
*/
function flushPreinstalls() {
- const Gerrit = window.Gerrit as GerritGlobal;
+ const Gerrit = window.Gerrit;
if (Gerrit.flushPreinstalls) {
Gerrit.flushPreinstalls();
}
@@ -75,9 +98,9 @@
export const _testOnly_flushPreinstalls = flushPreinstalls;
export function initGerritPluginApi() {
- window.Gerrit = {};
+ window.Gerrit = (window.Gerrit || {}) as GerritGlobal;
flushPreinstalls();
- initGerritPluginsMethods(window.Gerrit as GerritGlobal);
+ initGerritPluginsMethods(window.Gerrit);
// Preloaded plugins should be installed after Gerrit.install() is set,
// since plugin preloader substitutes Gerrit.install() temporarily.
// (Gerrit.install() is set in initGerritPluginsMethods)
@@ -86,7 +109,7 @@
export function _testOnly_initGerritPluginApi(): GerritGlobal {
initGerritPluginApi();
- return window.Gerrit as GerritGlobal;
+ return window.Gerrit;
}
export function deprecatedDelete(
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface-element.ts b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface-element.ts
index 33ad3ca..ff9446c 100644
--- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface-element.ts
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface-element.ts
@@ -247,12 +247,12 @@
return revertSubmissionMsg;
}
- getDiffLayers(path: string, changeNum: number, patchNum: number) {
+ getDiffLayers(path: string, changeNum: number) {
const layers = [];
for (const cb of this._getEventCallbacks(EventType.ANNOTATE_DIFF)) {
const annotationApi = (cb as unknown) as GrAnnotationActionsInterface;
try {
- const layer = annotationApi.getLayer(path, changeNum, patchNum);
+ const layer = annotationApi.getLayer(path, changeNum);
layers.push(layer);
} catch (err) {
console.error(err);
diff --git a/polygerrit-ui/app/elements/shared/gr-label-info/gr-label-info_html.ts b/polygerrit-ui/app/elements/shared/gr-label-info/gr-label-info_html.ts
index a2d1e1a..3955cd4 100644
--- a/polygerrit-ui/app/elements/shared/gr-label-info/gr-label-info_html.ts
+++ b/polygerrit-ui/app/elements/shared/gr-label-info/gr-label-info_html.ts
@@ -23,7 +23,6 @@
<style include="shared-styles">
.placeholder {
color: var(--deemphasized-text-color);
- padding-top: var(--spacing-xs);
}
.hidden {
display: none;
diff --git a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.ts b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.ts
index a140f50..f4111e7 100644
--- a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.ts
+++ b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.ts
@@ -1125,7 +1125,7 @@
return this._restApiHelper.fetchJSON({
url: `/accounts/${encodeURIComponent(userId)}/status`,
anonymizedUrl: '/accounts/*/status',
- });
+ }) as Promise<string | undefined>;
}
// https://gerrit-review.googlesource.com/Documentation/rest-api-accounts.html#list-groups
diff --git a/polygerrit-ui/app/run_test.sh b/polygerrit-ui/app/run_test.sh
index 2ca1118..0ceca3c 100755
--- a/polygerrit-ui/app/run_test.sh
+++ b/polygerrit-ui/app/run_test.sh
@@ -6,6 +6,11 @@
bazel_bin=bazel
fi
+# At least temporarily we want to know what is going on even when all tests are
+# passing, so we have a better chance of debugging what happens in CI test runs
+# that were supposed to catch test failures, but did not.
${bazel_bin} test \
"$@" \
+ --test_verbose_timeout_warnings \
+ --test_output=all \
//polygerrit-ui:karma_test
diff --git a/polygerrit-ui/app/services/gr-reporting/gr-reporting_impl.ts b/polygerrit-ui/app/services/gr-reporting/gr-reporting_impl.ts
index d3b652a..29cdd1e 100644
--- a/polygerrit-ui/app/services/gr-reporting/gr-reporting_impl.ts
+++ b/polygerrit-ui/app/services/gr-reporting/gr-reporting_impl.ts
@@ -155,6 +155,7 @@
// TODO(dmfilippov): TS-fix-any unclear what is context
const catchErrors = function (opt_context?: any) {
const context = opt_context || window;
+ const oldOnError = context.onerror;
context.onerror = (
event: Event | string,
source?: string,
@@ -162,7 +163,7 @@
colno?: number,
error?: Error
) => {
- return onError(context.onerror, event, source, lineno, colno, error);
+ return onError(oldOnError, event, source, lineno, colno, error);
};
context.addEventListener(
'unhandledrejection',
diff --git a/polygerrit-ui/app/services/services/gr-rest-api/gr-rest-api.ts b/polygerrit-ui/app/services/services/gr-rest-api/gr-rest-api.ts
index 9fb2882..90112b9 100644
--- a/polygerrit-ui/app/services/services/gr-rest-api/gr-rest-api.ts
+++ b/polygerrit-ui/app/services/services/gr-rest-api/gr-rest-api.ts
@@ -74,6 +74,7 @@
NameToProjectInfoMap,
ProjectInput,
AccountId,
+ ChangeMessageId,
} from '../../../types/common';
import {ParsedChangeInfo} from '../../../elements/shared/gr-rest-api-interface/gr-reviewer-updates-parser';
import {HttpMethod} from '../../../constants/constants';
@@ -533,6 +534,10 @@
getAccountGroups(): Promise<GroupInfo[] | undefined>;
+ getAccountDetails(userId: AccountId): Promise<AccountDetailInfo | undefined>;
+
+ getAccountStatus(userId: AccountId): Promise<string | undefined>;
+
saveAccountAgreement(name: ContributorAgreementInput): Promise<Response>;
generateAccountHttpPassword(): Promise<Password>;
@@ -595,4 +600,9 @@
account: AccountId,
label: string
): Promise<Response>;
+
+ deleteChangeCommitMessage(
+ changeNum: ChangeNum,
+ messageId: ChangeMessageId
+ ): Promise<Response>;
}
diff --git a/polygerrit-ui/app/test/common-test-setup.js b/polygerrit-ui/app/test/common-test-setup.js
index 004daf7..500187a 100644
--- a/polygerrit-ui/app/test/common-test-setup.js
+++ b/polygerrit-ui/app/test/common-test-setup.js
@@ -31,6 +31,7 @@
import sinon from 'sinon/pkg/sinon-esm.js';
import {safeTypesBridge} from '../utils/safe-types-util.js';
import {_testOnly_initGerritPluginApi} from '../elements/shared/gr-js-api-interface/gr-gerrit.js';
+import {initGlobalVariables} from '../elements/gr-app-global-var-init.js';
window.sinon = sinon;
security.polymer_resin.install({
@@ -60,6 +61,9 @@
};
setup(() => {
+ window.Gerrit = {};
+ initGlobalVariables();
+
// If the following asserts fails - then window.stub is
// overwritten by some other code.
assert.equal(cleanups.length, 0);
diff --git a/polygerrit-ui/app/types/common.ts b/polygerrit-ui/app/types/common.ts
index 52adb7b..e131b4f 100644
--- a/polygerrit-ui/app/types/common.ts
+++ b/polygerrit-ui/app/types/common.ts
@@ -410,6 +410,8 @@
export interface ChangeMessageInfo {
id: ChangeMessageId;
author?: AccountInfo;
+ reviewer?: AccountInfo;
+ updated_by?: AccountInfo;
real_author?: AccountInfo;
date: Timestamp;
message: string;
diff --git a/polygerrit-ui/app/types/globals.ts b/polygerrit-ui/app/types/globals.ts
index bdfb8c4..645c991 100644
--- a/polygerrit-ui/app/types/globals.ts
+++ b/polygerrit-ui/app/types/globals.ts
@@ -36,7 +36,7 @@
): void;
ASSETS_PATH?: string;
// TODO(TS): define gerrit type
- Gerrit?: unknown;
+ Gerrit?: any;
// TODO(TS): define polymer type
Polymer?: unknown;
// TODO(TS): remove page when better workaround is found
@@ -51,6 +51,56 @@
dashboardPage?: string;
};
STATIC_RESOURCE_PATH?: string;
+
+ /** Enhancements on Gr elements or utils */
+ // TODO(TS): should clean up those and removing them may break certain plugin behaviors
+ // TODO(TS): as @brohlfs suggested, to avoid importing anything from elements/ to types/
+ // use any for them for now
+ GrDisplayNameUtils: any;
+ GrAnnotation: any;
+ GrAttributeHelper: any;
+ GrDiffLine: any;
+ GrDiffLineType: any;
+ GrDiffGroup: any;
+ GrDiffGroupType: any;
+ GrDiffBuilder: any;
+ GrDiffBuilderSideBySide: any;
+ GrDiffBuilderImage: any;
+ GrDiffBuilderUnified: any;
+ GrDiffBuilderBinary: any;
+ GrChangeActionsInterface: any;
+ GrChangeReplyInterface: any;
+ GrEditConstants: any;
+ GrDomHooksManager: any;
+ GrDomHook: any;
+ GrEtagDecorator: any;
+ GrThemeApi: any;
+ SiteBasedCache: any;
+ FetchPromisesCache: any;
+ GrRestApiHelper: any;
+ GrLinkTextParser: any;
+ GrPluginEndpoints: any;
+ GrReviewerUpdatesParser: any;
+ GrPopupInterface: any;
+ GrCountStringFormatter: any;
+ GrReviewerSuggestionsProvider: any;
+ util: any;
+ Auth: any;
+ EventEmitter: any;
+ GrAdminApi: any;
+ GrAnnotationActionsContext: any;
+ GrAnnotationActionsInterface: any;
+ GrChangeMetadataApi: any;
+ GrEmailSuggestionsProvider: any;
+ GrGroupSuggestionsProvider: any;
+ GrEventHelper: any;
+ GrPluginRestApi: any;
+ GrRepoApi: any;
+ GrSettingsApi: any;
+ GrStylesApi: any;
+ PluginLoader: any;
+ GrPluginActionContext: any;
+ _apiUtils: {};
}
interface Performance {
diff --git a/polygerrit-ui/app/types/types.ts b/polygerrit-ui/app/types/types.ts
index 44178c4..8dd9371 100644
--- a/polygerrit-ui/app/types/types.ts
+++ b/polygerrit-ui/app/types/types.ts
@@ -18,6 +18,7 @@
import {IronA11yAnnouncer} from '@polymer/iron-a11y-announcer/iron-a11y-announcer';
import {GrDiffLine} from '../elements/diff/gr-diff/gr-diff-line';
import {FlattenedNodesObserver} from '@polymer/polymer/lib/utils/flattened-nodes-observer';
+import {PaperInputElement} from '@polymer/paper-input/paper-input';
export function notUndefined<T>(x: T | undefined): x is T {
return x !== undefined;
@@ -60,6 +61,14 @@
}
/**
+ * We would like to access the the typed `nativeInput` of PaperInputElement, so
+ * we are creating this wrapper.
+ */
+export type PaperInputElementExt = PaperInputElement & {
+ $: {nativeInput?: Element};
+};
+
+/**
* If Polymer would have exported DomApiNative from its dom.js utility, then we
* would probably not need this type. We just use it for casting the return
* value of dom(element).