Merge "Change page load report to report all navigation and resource timings"
diff --git a/Documentation/dev-release-jgit.txt b/Documentation/dev-release-jgit.txt
deleted file mode 100644
index 7fbbb95..0000000
--- a/Documentation/dev-release-jgit.txt
+++ /dev/null
@@ -1,53 +0,0 @@
-:linkattrs:
-= Making a Snapshot Release of JGit
-
-This step is only necessary if we need to create an unofficial JGit
-snapshot release and publish it to the
-link:https://developers.google.com/storage/[Google Cloud Storage,role=external,window=_blank].
-
-[[prepare-environment]]
-== Prepare the Maven Environment
-
-First, make sure you have done the necessary
-link:dev-release-deploy-config.html#deploy-configuration-settings-xml[
-configuration in Maven `settings.xml`].
-
-To apply the necessary settings in JGit's `pom.xml`, follow the instructions
-in link:dev-release-deploy-config.html#deploy-configuration-subprojects[
-Configuration for Subprojects in `pom.xml`], or apply the provided diff by
-executing the following command in the JGit workspace:
-
-----
- git apply /path/to/gerrit/tools/jgit-snapshot-deploy-pom.diff
-----
-
-[[prepare-release]]
-== Prepare the Release
-
-Since JGit has its own release process we do not push any release tags. Instead
-we will use the output of `git describe` as the version of the current JGit
-snapshot.
-
-In the JGit workspace, execute the following command:
-
-----
- ./tools/version.sh --release $(git describe)
-----
-
-[[publish-release]]
-== Publish the Release
-
-To deploy the new snapshot, execute the following command in the JGit
-workspace:
-
-----
- mvn deploy
-----
-
-
-GERRIT
-------
-Part of link:index.html[Gerrit Code Review]
-
-SEARCHBOX
----------
diff --git a/java/com/google/gerrit/acceptance/AbstractDaemonTest.java b/java/com/google/gerrit/acceptance/AbstractDaemonTest.java
index 60127b9..7a84501 100644
--- a/java/com/google/gerrit/acceptance/AbstractDaemonTest.java
+++ b/java/com/google/gerrit/acceptance/AbstractDaemonTest.java
@@ -40,6 +40,7 @@
import com.google.common.jimfs.Jimfs;
import com.google.common.primitives.Chars;
import com.google.gerrit.acceptance.AcceptanceTestRequestScope.Context;
+import com.google.gerrit.acceptance.PushOneCommit.Result;
import com.google.gerrit.acceptance.testsuite.account.TestSshKeys;
import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations;
import com.google.gerrit.common.Nullable;
@@ -61,6 +62,7 @@
import com.google.gerrit.entities.Project;
import com.google.gerrit.entities.RefNames;
import com.google.gerrit.extensions.api.GerritApi;
+import com.google.gerrit.extensions.api.changes.ChangeApi;
import com.google.gerrit.extensions.api.changes.ReviewInput;
import com.google.gerrit.extensions.api.changes.RevisionApi;
import com.google.gerrit.extensions.api.changes.SubmittedTogetherInfo;
@@ -836,6 +838,10 @@
return gApi.changes().id(id).info();
}
+ protected ChangeApi change(Result r) throws RestApiException {
+ return gApi.changes().id(r.getChange().getId().get());
+ }
+
protected Optional<EditInfo> getEdit(String id) throws RestApiException {
return gApi.changes().id(id).edit().get();
}
diff --git a/java/com/google/gerrit/entities/AttentionStatus.java b/java/com/google/gerrit/entities/AttentionSetUpdate.java
similarity index 77%
rename from java/com/google/gerrit/entities/AttentionStatus.java
rename to java/com/google/gerrit/entities/AttentionSetUpdate.java
index c488ccd..45588722 100644
--- a/java/com/google/gerrit/entities/AttentionStatus.java
+++ b/java/com/google/gerrit/entities/AttentionSetUpdate.java
@@ -21,14 +21,14 @@
/**
* A single update to the attention set. To reconstruct the attention set these instances are parsed
* in reverse chronological order. Since each update contains all required information and
- * invalidates all previous state (hence the name -Status rather than -Update), only the most recent
- * record is relevant for each user.
+ * invalidates all previous state, only the most recent record is relevant for each user.
*
- * <p>See <a href="https://www.gerritcodereview.com/design-docs/attention-set.html">here</a> for
- * details.
+ * <p>See {@link com.google.gerrit.extensions.api.changes.AddToAttentionSetInput} and {@link
+ * com.google.gerrit.extensions.api.changes.RemoveFromAttentionSetInput} for the representation in
+ * the API.
*/
@AutoValue
-public abstract class AttentionStatus {
+public abstract class AttentionSetUpdate {
/** Users can be added to or removed from the attention set. */
public enum Operation {
@@ -56,17 +56,17 @@
* Create an instance from data read from NoteDB. This includes the timestamp taken from the
* commit.
*/
- public static AttentionStatus createFromRead(
+ public static AttentionSetUpdate createFromRead(
Instant timestamp, Account.Id account, Operation operation, String reason) {
- return new AutoValue_AttentionStatus(timestamp, account, operation, reason);
+ return new AutoValue_AttentionSetUpdate(timestamp, account, operation, reason);
}
/**
* Create an instance to be written to NoteDB. This has no timestamp because the timestamp of the
* commit will be used.
*/
- public static AttentionStatus createForWrite(
+ public static AttentionSetUpdate createForWrite(
Account.Id account, Operation operation, String reason) {
- return new AutoValue_AttentionStatus(null, account, operation, reason);
+ return new AutoValue_AttentionSetUpdate(null, account, operation, reason);
}
}
diff --git a/java/com/google/gerrit/extensions/api/changes/AddToAttentionSetInput.java b/java/com/google/gerrit/extensions/api/changes/AddToAttentionSetInput.java
new file mode 100644
index 0000000..39efc64
--- /dev/null
+++ b/java/com/google/gerrit/extensions/api/changes/AddToAttentionSetInput.java
@@ -0,0 +1,33 @@
+// 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.extensions.api.changes;
+
+/**
+ * Input at API level to add a user to the attention set.
+ *
+ * @see RemoveFromAttentionSetInput
+ * @see com.google.gerrit.extensions.common.AttentionSetEntry
+ */
+public class AddToAttentionSetInput {
+ public String user;
+ public String reason;
+
+ public AddToAttentionSetInput(String user, String reason) {
+ this.user = user;
+ this.reason = reason;
+ }
+
+ public AddToAttentionSetInput() {}
+}
diff --git a/java/com/google/gerrit/extensions/api/changes/AttentionSetApi.java b/java/com/google/gerrit/extensions/api/changes/AttentionSetApi.java
new file mode 100644
index 0000000..5086cd8
--- /dev/null
+++ b/java/com/google/gerrit/extensions/api/changes/AttentionSetApi.java
@@ -0,0 +1,35 @@
+// 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.extensions.api.changes;
+
+import com.google.gerrit.extensions.restapi.NotImplementedException;
+import com.google.gerrit.extensions.restapi.RestApiException;
+
+/** API for managing the attention set of a change. */
+public interface AttentionSetApi {
+
+ void remove(RemoveFromAttentionSetInput input) throws RestApiException;
+
+ /**
+ * A default implementation which allows source compatibility when adding new methods to the
+ * interface.
+ */
+ class NotImplemented implements AttentionSetApi {
+ @Override
+ public void remove(RemoveFromAttentionSetInput input) throws RestApiException {
+ throw new NotImplementedException();
+ }
+ }
+}
diff --git a/java/com/google/gerrit/extensions/api/changes/ChangeApi.java b/java/com/google/gerrit/extensions/api/changes/ChangeApi.java
index 96455a6..284d8f6 100644
--- a/java/com/google/gerrit/extensions/api/changes/ChangeApi.java
+++ b/java/com/google/gerrit/extensions/api/changes/ChangeApi.java
@@ -312,6 +312,16 @@
*/
Set<String> getHashtags() throws RestApiException;
+ /**
+ * Manage the attention set.
+ *
+ * @param id The account identifier.
+ */
+ AttentionSetApi attention(String id) throws RestApiException;
+
+ /** Adds a user to the attention set. */
+ AccountInfo addToAttentionSet(AddToAttentionSetInput input) throws RestApiException;
+
/** Set the assignee of a change. */
AccountInfo setAssignee(AssigneeInput input) throws RestApiException;
@@ -581,6 +591,16 @@
}
@Override
+ public AttentionSetApi attention(String id) throws RestApiException {
+ throw new NotImplementedException();
+ }
+
+ @Override
+ public AccountInfo addToAttentionSet(AddToAttentionSetInput input) throws RestApiException {
+ throw new NotImplementedException();
+ }
+
+ @Override
public AccountInfo setAssignee(AssigneeInput input) throws RestApiException {
throw new NotImplementedException();
}
diff --git a/java/com/google/gerrit/extensions/api/changes/RemoveFromAttentionSetInput.java b/java/com/google/gerrit/extensions/api/changes/RemoveFromAttentionSetInput.java
new file mode 100644
index 0000000..9212788
--- /dev/null
+++ b/java/com/google/gerrit/extensions/api/changes/RemoveFromAttentionSetInput.java
@@ -0,0 +1,33 @@
+// 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.extensions.api.changes;
+
+import com.google.gerrit.extensions.restapi.DefaultInput;
+
+/**
+ * Input at API level to remove a user from the attention set.
+ *
+ * @see AddToAttentionSetInput
+ * @see com.google.gerrit.extensions.common.AttentionSetEntry
+ */
+public class RemoveFromAttentionSetInput {
+ @DefaultInput public String reason;
+
+ public RemoveFromAttentionSetInput(String reason) {
+ this.reason = reason;
+ }
+
+ public RemoveFromAttentionSetInput() {}
+}
diff --git a/java/com/google/gerrit/extensions/common/AttentionSetEntry.java b/java/com/google/gerrit/extensions/common/AttentionSetEntry.java
new file mode 100644
index 0000000..356b38a
--- /dev/null
+++ b/java/com/google/gerrit/extensions/common/AttentionSetEntry.java
@@ -0,0 +1,39 @@
+// 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.extensions.common;
+
+import java.sql.Timestamp;
+
+/**
+ * Represents a single user included in the attention set. Used in the API. See {@link
+ * com.google.gerrit.entities.AttentionSetUpdate} for the internal representation.
+ *
+ * <p>See <a href="https://www.gerritcodereview.com/design-docs/attention-set.html">here</a> for
+ * background.
+ */
+public class AttentionSetEntry {
+ /** The user included in the attention set. */
+ public AccountInfo accountInfo;
+ /** The timestamp of the last update. */
+ public Timestamp lastUpdate;
+ /** The human readable reason why the user was added. */
+ public String reason;
+
+ public AttentionSetEntry(AccountInfo accountInfo, Timestamp lastUpdate, String reason) {
+ this.accountInfo = accountInfo;
+ this.lastUpdate = lastUpdate;
+ this.reason = reason;
+ }
+}
diff --git a/java/com/google/gerrit/extensions/common/ChangeInfo.java b/java/com/google/gerrit/extensions/common/ChangeInfo.java
index d4a8477..dce6fd1 100644
--- a/java/com/google/gerrit/extensions/common/ChangeInfo.java
+++ b/java/com/google/gerrit/extensions/common/ChangeInfo.java
@@ -22,13 +22,28 @@
import java.util.List;
import java.util.Map;
+/**
+ * Representation of a change used in the API. Internally {@link
+ * com.google.gerrit.server.query.change.ChangeData} and {@link com.google.gerrit.entities.Change}
+ * are used.
+ *
+ * <p>Many fields are actually nullable.
+ */
public class ChangeInfo {
// ActionJson#copy(List, ChangeInfo) must be adapted if new fields are added that are not
// protected by any ListChangesOption.
+
public String id;
public String project;
public String branch;
public String topic;
+ /**
+ * The <a href="https://www.gerritcodereview.com/design-docs/attention-set.html">attention set</a>
+ * for this change. Keyed by account ID. We don't use {@link
+ * com.google.gerrit.entities.Account.Id} to avoid a circular dependency.
+ */
+ public Map<Integer, AttentionSetEntry> attentionSet;
+
public AccountInfo assignee;
public Collection<String> hashtags;
public String changeId;
diff --git a/java/com/google/gerrit/server/ChangeMessagesUtil.java b/java/com/google/gerrit/server/ChangeMessagesUtil.java
index 5f00b69..dd48b93 100644
--- a/java/com/google/gerrit/server/ChangeMessagesUtil.java
+++ b/java/com/google/gerrit/server/ChangeMessagesUtil.java
@@ -49,6 +49,8 @@
public static final String TAG_RESTORE = AUTOGENERATED_TAG_PREFIX + "gerrit:restore";
public static final String TAG_REVERT = AUTOGENERATED_TAG_PREFIX + "gerrit:revert";
public static final String TAG_SET_ASSIGNEE = AUTOGENERATED_TAG_PREFIX + "gerrit:setAssignee";
+ public static final String TAG_UPDATE_ATTENTION_SET =
+ AUTOGENERATED_TAG_PREFIX + "gerrit:updateAttentionSet";
public static final String TAG_SET_DESCRIPTION =
AUTOGENERATED_TAG_PREFIX + "gerrit:setPsDescription";
public static final String TAG_SET_HASHTAGS = AUTOGENERATED_TAG_PREFIX + "gerrit:setHashtag";
diff --git a/java/com/google/gerrit/server/api/changes/AttentionSetApiImpl.java b/java/com/google/gerrit/server/api/changes/AttentionSetApiImpl.java
new file mode 100644
index 0000000..8dc44b7
--- /dev/null
+++ b/java/com/google/gerrit/server/api/changes/AttentionSetApiImpl.java
@@ -0,0 +1,51 @@
+// Copyright (C) 2020 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.api.changes;
+
+import static com.google.gerrit.server.api.ApiUtil.asRestApiException;
+
+import com.google.gerrit.extensions.api.changes.AttentionSetApi;
+import com.google.gerrit.extensions.api.changes.RemoveFromAttentionSetInput;
+import com.google.gerrit.extensions.restapi.RestApiException;
+import com.google.gerrit.server.change.AttentionSetEntryResource;
+import com.google.gerrit.server.restapi.change.RemoveFromAttentionSet;
+import com.google.inject.Inject;
+import com.google.inject.assistedinject.Assisted;
+
+public class AttentionSetApiImpl implements AttentionSetApi {
+ interface Factory {
+ AttentionSetApiImpl create(AttentionSetEntryResource attentionSetEntryResource);
+ }
+
+ private final RemoveFromAttentionSet removeFromAttentionSet;
+ private final AttentionSetEntryResource attentionSetEntryResource;
+
+ @Inject
+ AttentionSetApiImpl(
+ RemoveFromAttentionSet removeFromAttentionSet,
+ @Assisted AttentionSetEntryResource attentionSetEntryResource) {
+ this.removeFromAttentionSet = removeFromAttentionSet;
+ this.attentionSetEntryResource = attentionSetEntryResource;
+ }
+
+ @Override
+ public void remove(RemoveFromAttentionSetInput input) throws RestApiException {
+ try {
+ removeFromAttentionSet.apply(attentionSetEntryResource, input);
+ } catch (Exception e) {
+ throw asRestApiException("Cannot remove from attention set", e);
+ }
+ }
+}
diff --git a/java/com/google/gerrit/server/api/changes/ChangeApiImpl.java b/java/com/google/gerrit/server/api/changes/ChangeApiImpl.java
index 0d640d9..5122f8a 100644
--- a/java/com/google/gerrit/server/api/changes/ChangeApiImpl.java
+++ b/java/com/google/gerrit/server/api/changes/ChangeApiImpl.java
@@ -23,7 +23,9 @@
import com.google.gerrit.extensions.api.changes.AbandonInput;
import com.google.gerrit.extensions.api.changes.AddReviewerInput;
import com.google.gerrit.extensions.api.changes.AddReviewerResult;
+import com.google.gerrit.extensions.api.changes.AddToAttentionSetInput;
import com.google.gerrit.extensions.api.changes.AssigneeInput;
+import com.google.gerrit.extensions.api.changes.AttentionSetApi;
import com.google.gerrit.extensions.api.changes.ChangeApi;
import com.google.gerrit.extensions.api.changes.ChangeEditApi;
import com.google.gerrit.extensions.api.changes.ChangeMessageApi;
@@ -66,6 +68,8 @@
import com.google.gerrit.server.change.ChangeResource;
import com.google.gerrit.server.change.WorkInProgressOp;
import com.google.gerrit.server.restapi.change.Abandon;
+import com.google.gerrit.server.restapi.change.AddToAttentionSet;
+import com.google.gerrit.server.restapi.change.AttentionSet;
import com.google.gerrit.server.restapi.change.ChangeIncludedIn;
import com.google.gerrit.server.restapi.change.ChangeMessages;
import com.google.gerrit.server.restapi.change.Check;
@@ -147,6 +151,9 @@
private final Provider<GetChange> getChangeProvider;
private final PostHashtags postHashtags;
private final GetHashtags getHashtags;
+ private final AttentionSet attentionSet;
+ private final AttentionSetApiImpl.Factory attentionSetApi;
+ private final AddToAttentionSet addToAttentionSet;
private final PutAssignee putAssignee;
private final GetAssignee getAssignee;
private final GetPastAssignees getPastAssignees;
@@ -197,6 +204,9 @@
Provider<GetChange> getChangeProvider,
PostHashtags postHashtags,
GetHashtags getHashtags,
+ AttentionSet attentionSet,
+ AttentionSetApiImpl.Factory attentionSetApi,
+ AddToAttentionSet addToAttentionSet,
PutAssignee putAssignee,
GetAssignee getAssignee,
GetPastAssignees getPastAssignees,
@@ -245,6 +255,9 @@
this.getChangeProvider = getChangeProvider;
this.postHashtags = postHashtags;
this.getHashtags = getHashtags;
+ this.attentionSet = attentionSet;
+ this.attentionSetApi = attentionSetApi;
+ this.addToAttentionSet = addToAttentionSet;
this.putAssignee = putAssignee;
this.getAssignee = getAssignee;
this.getPastAssignees = getPastAssignees;
@@ -530,6 +543,24 @@
}
@Override
+ public AccountInfo addToAttentionSet(AddToAttentionSetInput input) throws RestApiException {
+ try {
+ return addToAttentionSet.apply(change, input).value();
+ } catch (Exception e) {
+ throw asRestApiException("Cannot add to attention set", e);
+ }
+ }
+
+ @Override
+ public AttentionSetApi attention(String id) throws RestApiException {
+ try {
+ return attentionSetApi.create(attentionSet.parse(change, IdString.fromDecoded(id)));
+ } catch (Exception e) {
+ throw asRestApiException("Cannot parse account", e);
+ }
+ }
+
+ @Override
public AccountInfo setAssignee(AssigneeInput input) throws RestApiException {
try {
return putAssignee.apply(change, input).value();
diff --git a/java/com/google/gerrit/server/api/changes/Module.java b/java/com/google/gerrit/server/api/changes/Module.java
index 0edd58a..f54d1fe 100644
--- a/java/com/google/gerrit/server/api/changes/Module.java
+++ b/java/com/google/gerrit/server/api/changes/Module.java
@@ -32,5 +32,6 @@
factory(RevisionReviewerApiImpl.Factory.class);
factory(ChangeEditApiImpl.Factory.class);
factory(ChangeMessageApiImpl.Factory.class);
+ factory(AttentionSetApiImpl.Factory.class);
}
}
diff --git a/java/com/google/gerrit/server/change/ActionJson.java b/java/com/google/gerrit/server/change/ActionJson.java
index e87cf70..6f28dad 100644
--- a/java/com/google/gerrit/server/change/ActionJson.java
+++ b/java/com/google/gerrit/server/change/ActionJson.java
@@ -89,14 +89,12 @@
return Lists.newArrayList(visitorSet);
}
- public ChangeInfo addChangeActions(ChangeInfo to, ChangeNotes notes) {
+ void addChangeActions(ChangeInfo to, ChangeNotes notes) {
List<ActionVisitor> visitors = visitors();
to.actions = toActionMap(notes, visitors, copy(visitors, to));
- return to;
}
- public RevisionInfo addRevisionActions(
- @Nullable ChangeInfo changeInfo, RevisionInfo to, RevisionResource rsrc) {
+ void addRevisionActions(@Nullable ChangeInfo changeInfo, RevisionInfo to, RevisionResource rsrc) {
List<ActionVisitor> visitors = visitors();
if (!visitors.isEmpty()) {
if (changeInfo != null) {
@@ -106,7 +104,6 @@
}
}
to.actions = toActionMap(rsrc, visitors, changeInfo, copy(visitors, to));
- return to;
}
private ChangeInfo copy(List<ActionVisitor> visitors, ChangeInfo changeInfo) {
@@ -119,6 +116,8 @@
copy.project = changeInfo.project;
copy.branch = changeInfo.branch;
copy.topic = changeInfo.topic;
+ copy.attentionSet =
+ changeInfo.attentionSet == null ? null : ImmutableMap.copyOf(changeInfo.attentionSet);
copy.assignee = changeInfo.assignee;
copy.hashtags = changeInfo.hashtags;
copy.changeId = changeInfo.changeId;
diff --git a/java/com/google/gerrit/server/change/AddToAttentionSetOp.java b/java/com/google/gerrit/server/change/AddToAttentionSetOp.java
new file mode 100644
index 0000000..262fdc2
--- /dev/null
+++ b/java/com/google/gerrit/server/change/AddToAttentionSetOp.java
@@ -0,0 +1,85 @@
+// Copyright (C) 2020 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.change;
+
+import static com.google.common.collect.ImmutableMap.toImmutableMap;
+import static java.util.Objects.requireNonNull;
+
+import com.google.common.collect.ImmutableList;
+import com.google.gerrit.entities.Account;
+import com.google.gerrit.entities.AttentionSetUpdate;
+import com.google.gerrit.entities.AttentionSetUpdate.Operation;
+import com.google.gerrit.extensions.restapi.RestApiException;
+import com.google.gerrit.server.ChangeMessagesUtil;
+import com.google.gerrit.server.notedb.ChangeUpdate;
+import com.google.gerrit.server.query.change.ChangeData;
+import com.google.gerrit.server.update.BatchUpdateOp;
+import com.google.gerrit.server.update.ChangeContext;
+import com.google.inject.Inject;
+import com.google.inject.assistedinject.Assisted;
+import java.util.Map;
+import java.util.function.Function;
+
+/** Add a specified user to the attention set. */
+public class AddToAttentionSetOp implements BatchUpdateOp {
+
+ public interface Factory {
+ AddToAttentionSetOp create(Account.Id attentionUserId, String reason);
+ }
+
+ private final ChangeData.Factory changeDataFactory;
+ private final ChangeMessagesUtil cmUtil;
+ private final Account.Id attentionUserId;
+ private final String reason;
+
+ @Inject
+ AddToAttentionSetOp(
+ ChangeData.Factory changeDataFactory,
+ ChangeMessagesUtil cmUtil,
+ @Assisted Account.Id attentionUserId,
+ @Assisted String reason) {
+ this.changeDataFactory = changeDataFactory;
+ this.cmUtil = cmUtil;
+ this.attentionUserId = requireNonNull(attentionUserId, "user");
+ this.reason = requireNonNull(reason, "reason");
+ }
+
+ @Override
+ public boolean updateChange(ChangeContext ctx) throws RestApiException {
+ ChangeData changeData = changeDataFactory.create(ctx.getNotes());
+ Map<Account.Id, AttentionSetUpdate> attentionMap =
+ changeData.attentionSet().stream()
+ .collect(toImmutableMap(AttentionSetUpdate::account, Function.identity()));
+ AttentionSetUpdate existingEntry = attentionMap.get(attentionUserId);
+ if (existingEntry != null && existingEntry.operation() == Operation.ADD) {
+ return false;
+ }
+
+ ChangeUpdate update = ctx.getUpdate(ctx.getChange().currentPatchSetId());
+ update.setAttentionSetUpdates(
+ ImmutableList.of(
+ AttentionSetUpdate.createForWrite(
+ attentionUserId, AttentionSetUpdate.Operation.ADD, reason)));
+ addMessage(ctx, update);
+ return true;
+ }
+
+ private void addMessage(ChangeContext ctx, ChangeUpdate update) {
+ String message = "Added to attention set: " + attentionUserId;
+ cmUtil.addChangeMessage(
+ update,
+ ChangeMessagesUtil.newMessage(ctx, message, ChangeMessagesUtil.TAG_UPDATE_ATTENTION_SET));
+ }
+}
diff --git a/java/com/google/gerrit/server/change/AttentionSetEntryResource.java b/java/com/google/gerrit/server/change/AttentionSetEntryResource.java
new file mode 100644
index 0000000..6c6c765
--- /dev/null
+++ b/java/com/google/gerrit/server/change/AttentionSetEntryResource.java
@@ -0,0 +1,46 @@
+// Copyright (C) 2020 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.change;
+
+import com.google.gerrit.entities.Account;
+import com.google.gerrit.extensions.restapi.RestResource;
+import com.google.gerrit.extensions.restapi.RestView;
+import com.google.inject.TypeLiteral;
+
+/** REST resource that represents an entry in the attention set of a change. */
+public class AttentionSetEntryResource implements RestResource {
+ public static final TypeLiteral<RestView<AttentionSetEntryResource>> ATTENTION_SET_ENTRY_KIND =
+ new TypeLiteral<RestView<AttentionSetEntryResource>>() {};
+
+ public interface Factory {
+ AttentionSetEntryResource create(ChangeResource change, Account.Id id);
+ }
+
+ private final ChangeResource changeResource;
+ private final Account.Id accountId;
+
+ public AttentionSetEntryResource(ChangeResource changeResource, Account.Id accountId) {
+ this.changeResource = changeResource;
+ this.accountId = accountId;
+ }
+
+ public ChangeResource getChangeResource() {
+ return changeResource;
+ }
+
+ public Account.Id getAccountId() {
+ return accountId;
+ }
+}
diff --git a/java/com/google/gerrit/server/change/ChangeJson.java b/java/com/google/gerrit/server/change/ChangeJson.java
index b704205..e4148a5 100644
--- a/java/com/google/gerrit/server/change/ChangeJson.java
+++ b/java/com/google/gerrit/server/change/ChangeJson.java
@@ -14,6 +14,7 @@
package com.google.gerrit.server.change;
+import static com.google.common.collect.ImmutableMap.toImmutableMap;
import static com.google.gerrit.extensions.client.ListChangesOption.ALL_COMMITS;
import static com.google.gerrit.extensions.client.ListChangesOption.ALL_REVISIONS;
import static com.google.gerrit.extensions.client.ListChangesOption.CHANGE_ACTIONS;
@@ -49,6 +50,7 @@
import com.google.gerrit.common.data.SubmitRequirement;
import com.google.gerrit.common.data.SubmitTypeRecord;
import com.google.gerrit.entities.Account;
+import com.google.gerrit.entities.AttentionSetUpdate;
import com.google.gerrit.entities.Change;
import com.google.gerrit.entities.ChangeMessage;
import com.google.gerrit.entities.PatchSet;
@@ -60,6 +62,7 @@
import com.google.gerrit.extensions.client.ReviewerState;
import com.google.gerrit.extensions.common.AccountInfo;
import com.google.gerrit.extensions.common.ApprovalInfo;
+import com.google.gerrit.extensions.common.AttentionSetEntry;
import com.google.gerrit.extensions.common.ChangeInfo;
import com.google.gerrit.extensions.common.ChangeMessageInfo;
import com.google.gerrit.extensions.common.LabelInfo;
@@ -102,6 +105,7 @@
import com.google.inject.Singleton;
import com.google.inject.assistedinject.Assisted;
import java.io.IOException;
+import java.sql.Timestamp;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
@@ -504,6 +508,20 @@
out.project = in.getProject().get();
out.branch = in.getDest().shortName();
out.topic = in.getTopic();
+ if (!cd.attentionSet().isEmpty()) {
+ out.attentionSet =
+ cd.attentionSet().stream()
+ // This filtering should match GetAttentionSet.
+ .filter(a -> a.operation() == AttentionSetUpdate.Operation.ADD)
+ .collect(
+ toImmutableMap(
+ a -> a.account().get(),
+ a ->
+ new AttentionSetEntry(
+ accountLoader.get(a.account()),
+ Timestamp.from(a.timestamp()),
+ a.reason())));
+ }
out.assignee = in.getAssignee() != null ? accountLoader.get(in.getAssignee()) : null;
out.hashtags = cd.hashtags();
out.changeId = in.getKey().get();
diff --git a/java/com/google/gerrit/server/change/RemoveFromAttentionSetOp.java b/java/com/google/gerrit/server/change/RemoveFromAttentionSetOp.java
new file mode 100644
index 0000000..7118089
--- /dev/null
+++ b/java/com/google/gerrit/server/change/RemoveFromAttentionSetOp.java
@@ -0,0 +1,84 @@
+// Copyright (C) 2020 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.change;
+
+import static com.google.common.collect.ImmutableMap.toImmutableMap;
+import static java.util.Objects.requireNonNull;
+
+import com.google.common.collect.ImmutableList;
+import com.google.gerrit.entities.Account;
+import com.google.gerrit.entities.AttentionSetUpdate;
+import com.google.gerrit.entities.AttentionSetUpdate.Operation;
+import com.google.gerrit.extensions.restapi.RestApiException;
+import com.google.gerrit.server.ChangeMessagesUtil;
+import com.google.gerrit.server.notedb.ChangeUpdate;
+import com.google.gerrit.server.query.change.ChangeData;
+import com.google.gerrit.server.update.BatchUpdateOp;
+import com.google.gerrit.server.update.ChangeContext;
+import com.google.inject.Inject;
+import com.google.inject.assistedinject.Assisted;
+import java.util.Map;
+import java.util.function.Function;
+
+/** Remove a specified user from the attention set. */
+public class RemoveFromAttentionSetOp implements BatchUpdateOp {
+
+ public interface Factory {
+ RemoveFromAttentionSetOp create(Account.Id attentionUserId, String reason);
+ }
+
+ private final ChangeData.Factory changeDataFactory;
+ private final ChangeMessagesUtil cmUtil;
+ private final Account.Id attentionUserId;
+ private final String reason;
+
+ @Inject
+ RemoveFromAttentionSetOp(
+ ChangeData.Factory changeDataFactory,
+ ChangeMessagesUtil cmUtil,
+ @Assisted Account.Id attentionUserId,
+ @Assisted String reason) {
+ this.changeDataFactory = changeDataFactory;
+ this.cmUtil = cmUtil;
+ this.attentionUserId = requireNonNull(attentionUserId, "user");
+ this.reason = requireNonNull(reason, "reason");
+ }
+
+ @Override
+ public boolean updateChange(ChangeContext ctx) throws RestApiException {
+ ChangeData changeData = changeDataFactory.create(ctx.getNotes());
+ Map<Account.Id, AttentionSetUpdate> attentionMap =
+ changeData.attentionSet().stream()
+ .collect(toImmutableMap(AttentionSetUpdate::account, Function.identity()));
+ AttentionSetUpdate existingEntry = attentionMap.get(attentionUserId);
+ if (existingEntry == null || existingEntry.operation() == Operation.REMOVE) {
+ return false;
+ }
+
+ ChangeUpdate update = ctx.getUpdate(ctx.getChange().currentPatchSetId());
+ update.setAttentionSetUpdates(
+ ImmutableList.of(
+ AttentionSetUpdate.createForWrite(attentionUserId, Operation.REMOVE, reason)));
+ addMessage(ctx, update);
+ return true;
+ }
+
+ private void addMessage(ChangeContext ctx, ChangeUpdate update) {
+ String message = "Removed from attention set: " + attentionUserId;
+ cmUtil.addChangeMessage(
+ update,
+ ChangeMessagesUtil.newMessage(ctx, message, ChangeMessagesUtil.TAG_UPDATE_ATTENTION_SET));
+ }
+}
diff --git a/java/com/google/gerrit/server/notedb/ChangeNoteUtil.java b/java/com/google/gerrit/server/notedb/ChangeNoteUtil.java
index 287f3e7..86b6ed7 100644
--- a/java/com/google/gerrit/server/notedb/ChangeNoteUtil.java
+++ b/java/com/google/gerrit/server/notedb/ChangeNoteUtil.java
@@ -16,7 +16,7 @@
import com.google.auto.value.AutoValue;
import com.google.gerrit.entities.Account;
-import com.google.gerrit.entities.AttentionStatus;
+import com.google.gerrit.entities.AttentionSetUpdate;
import com.google.gerrit.json.OutputFormat;
import com.google.gerrit.server.config.GerritServerId;
import com.google.gson.Gson;
@@ -171,11 +171,11 @@
private static class AttentionStatusInNoteDb {
final String personIdent;
- final AttentionStatus.Operation operation;
+ final AttentionSetUpdate.Operation operation;
final String reason;
AttentionStatusInNoteDb(
- String personIndent, AttentionStatus.Operation operation, String reason) {
+ String personIndent, AttentionSetUpdate.Operation operation, String reason) {
this.personIdent = personIndent;
this.operation = operation;
this.reason = reason;
@@ -183,7 +183,7 @@
}
/** The returned {@link Optional} holds the parsed entity or is empty if parsing failed. */
- static Optional<AttentionStatus> attentionStatusFromJson(
+ static Optional<AttentionSetUpdate> attentionStatusFromJson(
Instant timestamp, String attentionString) {
AttentionStatusInNoteDb inNoteDb =
gson.fromJson(attentionString, AttentionStatusInNoteDb.class);
@@ -193,18 +193,20 @@
}
Optional<Account.Id> account = NoteDbUtil.parseIdent(personIdent);
return account.map(
- id -> AttentionStatus.createFromRead(timestamp, id, inNoteDb.operation, inNoteDb.reason));
+ id ->
+ AttentionSetUpdate.createFromRead(timestamp, id, inNoteDb.operation, inNoteDb.reason));
}
- String attentionStatusToJson(AttentionStatus attentionStatus) {
+ String attentionSetUpdateToJson(AttentionSetUpdate attentionSetUpdate) {
PersonIdent personIdent =
new PersonIdent(
- getUsername(attentionStatus.account()), getEmailAddress(attentionStatus.account()));
+ getUsername(attentionSetUpdate.account()),
+ getEmailAddress(attentionSetUpdate.account()));
StringBuilder stringBuilder = new StringBuilder();
appendIdentString(stringBuilder, personIdent.getName(), personIdent.getEmailAddress());
return gson.toJson(
new AttentionStatusInNoteDb(
- stringBuilder.toString(), attentionStatus.operation(), attentionStatus.reason()));
+ stringBuilder.toString(), attentionSetUpdate.operation(), attentionSetUpdate.reason()));
}
static void appendIdentString(StringBuilder stringBuilder, String name, String emailAddress) {
diff --git a/java/com/google/gerrit/server/notedb/ChangeNotes.java b/java/com/google/gerrit/server/notedb/ChangeNotes.java
index 2de2195..84bd29b 100644
--- a/java/com/google/gerrit/server/notedb/ChangeNotes.java
+++ b/java/com/google/gerrit/server/notedb/ChangeNotes.java
@@ -40,7 +40,7 @@
import com.google.gerrit.common.Nullable;
import com.google.gerrit.common.data.SubmitRecord;
import com.google.gerrit.entities.Account;
-import com.google.gerrit.entities.AttentionStatus;
+import com.google.gerrit.entities.AttentionSetUpdate;
import com.google.gerrit.entities.BranchNameKey;
import com.google.gerrit.entities.Change;
import com.google.gerrit.entities.ChangeMessage;
@@ -376,8 +376,9 @@
return state.reviewerUpdates();
}
- public ImmutableList<AttentionStatus> getAttentionUpdates() {
- return state.attentionUpdates();
+ /** Returns the most recent update (i.e. status) per user. */
+ public ImmutableList<AttentionSetUpdate> getAttentionSet() {
+ return state.attentionSet();
}
/**
diff --git a/java/com/google/gerrit/server/notedb/ChangeNotesParser.java b/java/com/google/gerrit/server/notedb/ChangeNotesParser.java
index ed6039f..6b6a7ca 100644
--- a/java/com/google/gerrit/server/notedb/ChangeNotesParser.java
+++ b/java/com/google/gerrit/server/notedb/ChangeNotesParser.java
@@ -59,7 +59,7 @@
import com.google.gerrit.common.data.LabelType;
import com.google.gerrit.common.data.SubmitRecord;
import com.google.gerrit.entities.Account;
-import com.google.gerrit.entities.AttentionStatus;
+import com.google.gerrit.entities.AttentionSetUpdate;
import com.google.gerrit.entities.Change;
import com.google.gerrit.entities.ChangeMessage;
import com.google.gerrit.entities.Comment;
@@ -118,7 +118,7 @@
private final List<Account.Id> allPastReviewers;
private final List<ReviewerStatusUpdate> reviewerUpdates;
/** Holds only the most recent update per user. Older updates are discarded. */
- private final Map<Account.Id, AttentionStatus> latestAttentionStatus;
+ private final Map<Account.Id, AttentionSetUpdate> latestAttentionStatus;
private final List<AssigneeStatusUpdate> assigneeUpdates;
private final List<SubmitRecord> submitRecords;
@@ -367,7 +367,7 @@
}
parseHashtags(commit);
- parseAttentionUpdates(commit);
+ parseAttentionSetUpdates(commit);
parseAssigneeUpdates(ts, commit);
if (submissionId == null) {
@@ -578,11 +578,11 @@
}
}
- private void parseAttentionUpdates(ChangeNotesCommit commit) throws ConfigInvalidException {
+ private void parseAttentionSetUpdates(ChangeNotesCommit commit) throws ConfigInvalidException {
List<String> attentionStrings = commit.getFooterLineValues(FOOTER_ATTENTION);
for (String attentionString : attentionStrings) {
- Optional<AttentionStatus> attentionStatus =
+ Optional<AttentionSetUpdate> attentionStatus =
ChangeNoteUtil.attentionStatusFromJson(
Instant.ofEpochSecond(commit.getCommitTime()), attentionString);
if (!attentionStatus.isPresent()) {
diff --git a/java/com/google/gerrit/server/notedb/ChangeNotesState.java b/java/com/google/gerrit/server/notedb/ChangeNotesState.java
index 67faa33..9cd4af3 100644
--- a/java/com/google/gerrit/server/notedb/ChangeNotesState.java
+++ b/java/com/google/gerrit/server/notedb/ChangeNotesState.java
@@ -35,7 +35,7 @@
import com.google.gerrit.common.Nullable;
import com.google.gerrit.common.data.SubmitRecord;
import com.google.gerrit.entities.Account;
-import com.google.gerrit.entities.AttentionStatus;
+import com.google.gerrit.entities.AttentionSetUpdate;
import com.google.gerrit.entities.BranchNameKey;
import com.google.gerrit.entities.Change;
import com.google.gerrit.entities.ChangeMessage;
@@ -56,7 +56,7 @@
import com.google.gerrit.server.ReviewerStatusUpdate;
import com.google.gerrit.server.cache.proto.Cache.ChangeNotesStateProto;
import com.google.gerrit.server.cache.proto.Cache.ChangeNotesStateProto.AssigneeStatusUpdateProto;
-import com.google.gerrit.server.cache.proto.Cache.ChangeNotesStateProto.AttentionStatusProto;
+import com.google.gerrit.server.cache.proto.Cache.ChangeNotesStateProto.AttentionSetUpdateProto;
import com.google.gerrit.server.cache.proto.Cache.ChangeNotesStateProto.ChangeColumnsProto;
import com.google.gerrit.server.cache.proto.Cache.ChangeNotesStateProto.ReviewerByEmailSetEntryProto;
import com.google.gerrit.server.cache.proto.Cache.ChangeNotesStateProto.ReviewerSetEntryProto;
@@ -119,7 +119,7 @@
ReviewerByEmailSet pendingReviewersByEmail,
List<Account.Id> allPastReviewers,
List<ReviewerStatusUpdate> reviewerUpdates,
- List<AttentionStatus> attentionStatusUpdates,
+ List<AttentionSetUpdate> attentionSetUpdates,
List<AssigneeStatusUpdate> assigneeUpdates,
List<SubmitRecord> submitRecords,
List<ChangeMessage> changeMessages,
@@ -170,7 +170,7 @@
.pendingReviewersByEmail(pendingReviewersByEmail)
.allPastReviewers(allPastReviewers)
.reviewerUpdates(reviewerUpdates)
- .attentionUpdates(attentionStatusUpdates)
+ .attentionSet(attentionSetUpdates)
.assigneeUpdates(assigneeUpdates)
.submitRecords(submitRecords)
.changeMessages(changeMessages)
@@ -305,7 +305,8 @@
abstract ImmutableList<ReviewerStatusUpdate> reviewerUpdates();
- abstract ImmutableList<AttentionStatus> attentionUpdates();
+ /** Returns the most recent update (i.e. current status status) per user. */
+ abstract ImmutableList<AttentionSetUpdate> attentionSet();
abstract ImmutableList<AssigneeStatusUpdate> assigneeUpdates();
@@ -384,7 +385,7 @@
.pendingReviewersByEmail(ReviewerByEmailSet.empty())
.allPastReviewers(ImmutableList.of())
.reviewerUpdates(ImmutableList.of())
- .attentionUpdates(ImmutableList.of())
+ .attentionSet(ImmutableList.of())
.assigneeUpdates(ImmutableList.of())
.submitRecords(ImmutableList.of())
.changeMessages(ImmutableList.of())
@@ -418,7 +419,7 @@
abstract Builder reviewerUpdates(List<ReviewerStatusUpdate> reviewerUpdates);
- abstract Builder attentionUpdates(List<AttentionStatus> attentionUpdates);
+ abstract Builder attentionSet(List<AttentionSetUpdate> attentionSetUpdates);
abstract Builder assigneeUpdates(List<AssigneeStatusUpdate> assigneeUpdates);
@@ -487,7 +488,7 @@
object.allPastReviewers().forEach(a -> b.addPastReviewer(a.get()));
object.reviewerUpdates().forEach(u -> b.addReviewerUpdate(toReviewerStatusUpdateProto(u)));
- object.attentionUpdates().forEach(u -> b.addAttentionStatus(toAttentionStatusProto(u)));
+ object.attentionSet().forEach(u -> b.addAttentionSetUpdate(toAttentionSetUpdateProto(u)));
object.assigneeUpdates().forEach(u -> b.addAssigneeUpdate(toAssigneeStatusUpdateProto(u)));
object
.submitRecords()
@@ -571,12 +572,13 @@
.build();
}
- private static AttentionStatusProto toAttentionStatusProto(AttentionStatus attentionStatus) {
- return AttentionStatusProto.newBuilder()
- .setTimestampMillis(attentionStatus.timestamp().toEpochMilli())
- .setAccount(attentionStatus.account().get())
- .setOperation(attentionStatus.operation().name())
- .setReason(attentionStatus.reason())
+ private static AttentionSetUpdateProto toAttentionSetUpdateProto(
+ AttentionSetUpdate attentionSetUpdate) {
+ return AttentionSetUpdateProto.newBuilder()
+ .setTimestampMillis(attentionSetUpdate.timestamp().toEpochMilli())
+ .setAccount(attentionSetUpdate.account().get())
+ .setOperation(attentionSetUpdate.operation().name())
+ .setReason(attentionSetUpdate.reason())
.build();
}
@@ -620,7 +622,7 @@
.allPastReviewers(
proto.getPastReviewerList().stream().map(Account::id).collect(toImmutableList()))
.reviewerUpdates(toReviewerStatusUpdateList(proto.getReviewerUpdateList()))
- .attentionUpdates(toAttentionUpdateList(proto.getAttentionStatusList()))
+ .attentionSet(toAttentionSetUpdateList(proto.getAttentionSetUpdateList()))
.assigneeUpdates(toAssigneeStatusUpdateList(proto.getAssigneeUpdateList()))
.submitRecords(
proto.getSubmitRecordList().stream()
@@ -719,15 +721,15 @@
return b.build();
}
- private static ImmutableList<AttentionStatus> toAttentionUpdateList(
- List<AttentionStatusProto> protos) {
- ImmutableList.Builder<AttentionStatus> b = ImmutableList.builder();
- for (AttentionStatusProto proto : protos) {
+ private static ImmutableList<AttentionSetUpdate> toAttentionSetUpdateList(
+ List<AttentionSetUpdateProto> protos) {
+ ImmutableList.Builder<AttentionSetUpdate> b = ImmutableList.builder();
+ for (AttentionSetUpdateProto proto : protos) {
b.add(
- AttentionStatus.createFromRead(
+ AttentionSetUpdate.createFromRead(
Instant.ofEpochMilli(proto.getTimestampMillis()),
Account.id(proto.getAccount()),
- AttentionStatus.Operation.valueOf(proto.getOperation()),
+ AttentionSetUpdate.Operation.valueOf(proto.getOperation()),
proto.getReason()));
}
return b.build();
diff --git a/java/com/google/gerrit/server/notedb/ChangeUpdate.java b/java/com/google/gerrit/server/notedb/ChangeUpdate.java
index 4492050..0de090f 100644
--- a/java/com/google/gerrit/server/notedb/ChangeUpdate.java
+++ b/java/com/google/gerrit/server/notedb/ChangeUpdate.java
@@ -54,7 +54,7 @@
import com.google.common.collect.TreeBasedTable;
import com.google.gerrit.common.data.SubmitRecord;
import com.google.gerrit.entities.Account;
-import com.google.gerrit.entities.AttentionStatus;
+import com.google.gerrit.entities.AttentionSetUpdate;
import com.google.gerrit.entities.Change;
import com.google.gerrit.entities.Comment;
import com.google.gerrit.entities.Project;
@@ -128,7 +128,7 @@
private String submissionId;
private String topic;
private String commit;
- private List<AttentionStatus> attentionUpdates;
+ private List<AttentionSetUpdate> attentionSetUpdates;
private Optional<Account.Id> assignee;
private Set<String> hashtags;
private String changeMessage;
@@ -369,15 +369,15 @@
* All updates must have a timestamp of null since we use the commit's timestamp. There also must
* not be multiple updates for a single user.
*/
- void setAttentionUpdates(List<AttentionStatus> attentionUpdates) {
+ public void setAttentionSetUpdates(List<AttentionSetUpdate> attentionSetUpdates) {
checkArgument(
- attentionUpdates.stream().noneMatch(x -> x.timestamp() != null),
+ attentionSetUpdates.stream().noneMatch(a -> a.timestamp() != null),
"must not specify timestamp for write");
checkArgument(
- attentionUpdates.stream().map(AttentionStatus::account).distinct().count()
- == attentionUpdates.size(),
+ attentionSetUpdates.stream().map(AttentionSetUpdate::account).distinct().count()
+ == attentionSetUpdates.size(),
"must not specify multiple updates for single user");
- this.attentionUpdates = attentionUpdates;
+ this.attentionSetUpdates = attentionSetUpdates;
}
public void setAssignee(Account.Id assignee) {
@@ -588,9 +588,9 @@
addFooter(msg, FOOTER_COMMIT, commit);
}
- if (attentionUpdates != null) {
- for (AttentionStatus attentionUpdate : attentionUpdates) {
- addFooter(msg, FOOTER_ATTENTION, noteUtil.attentionStatusToJson(attentionUpdate));
+ if (attentionSetUpdates != null) {
+ for (AttentionSetUpdate attentionSetUpdate : attentionSetUpdates) {
+ addFooter(msg, FOOTER_ATTENTION, noteUtil.attentionSetUpdateToJson(attentionSetUpdate));
}
}
@@ -730,7 +730,7 @@
&& status == null
&& submissionId == null
&& submitRecords == null
- && attentionUpdates == null
+ && attentionSetUpdates == null
&& assignee == null
&& hashtags == null
&& topic == null
diff --git a/java/com/google/gerrit/server/query/change/ChangeData.java b/java/com/google/gerrit/server/query/change/ChangeData.java
index 563ebb7..3dce678 100644
--- a/java/com/google/gerrit/server/query/change/ChangeData.java
+++ b/java/com/google/gerrit/server/query/change/ChangeData.java
@@ -37,6 +37,7 @@
import com.google.gerrit.common.data.SubmitRecord;
import com.google.gerrit.common.data.SubmitTypeRecord;
import com.google.gerrit.entities.Account;
+import com.google.gerrit.entities.AttentionSetUpdate;
import com.google.gerrit.entities.Change;
import com.google.gerrit.entities.ChangeMessage;
import com.google.gerrit.entities.Comment;
@@ -297,6 +298,7 @@
private List<ReviewerStatusUpdate> reviewerUpdates;
private PersonIdent author;
private PersonIdent committer;
+ private ImmutableList<AttentionSetUpdate> attentionSet;
private int parentCount;
private Integer unresolvedCommentCount;
private Integer totalCommentCount;
@@ -598,6 +600,17 @@
return true;
}
+ /** Returns the most recent update (i.e. status) per user. */
+ public ImmutableList<AttentionSetUpdate> attentionSet() {
+ if (attentionSet == null) {
+ if (!lazyLoad) {
+ return ImmutableList.of();
+ }
+ attentionSet = notes().getAttentionSet();
+ }
+ return attentionSet;
+ }
+
/** @return patches for the change, in patch set ID order. */
public Collection<PatchSet> patchSets() {
if (patchSets == null) {
diff --git a/java/com/google/gerrit/server/restapi/change/AddToAttentionSet.java b/java/com/google/gerrit/server/restapi/change/AddToAttentionSet.java
new file mode 100644
index 0000000..5176fe9
--- /dev/null
+++ b/java/com/google/gerrit/server/restapi/change/AddToAttentionSet.java
@@ -0,0 +1,93 @@
+// Copyright (C) 2020 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.restapi.change;
+
+import com.google.common.base.Strings;
+import com.google.gerrit.entities.Account;
+import com.google.gerrit.extensions.api.changes.AddToAttentionSetInput;
+import com.google.gerrit.extensions.common.AccountInfo;
+import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.extensions.restapi.BadRequestException;
+import com.google.gerrit.extensions.restapi.Response;
+import com.google.gerrit.extensions.restapi.RestCollectionModifyView;
+import com.google.gerrit.server.account.AccountLoader;
+import com.google.gerrit.server.account.AccountResolver;
+import com.google.gerrit.server.change.AddToAttentionSetOp;
+import com.google.gerrit.server.change.AttentionSetEntryResource;
+import com.google.gerrit.server.change.ChangeResource;
+import com.google.gerrit.server.permissions.ChangePermission;
+import com.google.gerrit.server.permissions.PermissionBackend;
+import com.google.gerrit.server.update.BatchUpdate;
+import com.google.gerrit.server.util.time.TimeUtil;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+
+/** Adds a single user to the attention set. */
+@Singleton
+public class AddToAttentionSet
+ implements RestCollectionModifyView<
+ ChangeResource, AttentionSetEntryResource, AddToAttentionSetInput> {
+ private final BatchUpdate.Factory updateFactory;
+ private final AccountResolver accountResolver;
+ private final AddToAttentionSetOp.Factory opFactory;
+ private final AccountLoader.Factory accountLoaderFactory;
+ private final PermissionBackend permissionBackend;
+
+ @Inject
+ AddToAttentionSet(
+ BatchUpdate.Factory updateFactory,
+ AccountResolver accountResolver,
+ AddToAttentionSetOp.Factory opFactory,
+ AccountLoader.Factory accountLoaderFactory,
+ PermissionBackend permissionBackend) {
+ this.updateFactory = updateFactory;
+ this.accountResolver = accountResolver;
+ this.opFactory = opFactory;
+ this.accountLoaderFactory = accountLoaderFactory;
+ this.permissionBackend = permissionBackend;
+ }
+
+ @Override
+ public Response<AccountInfo> apply(ChangeResource changeResource, AddToAttentionSetInput input)
+ throws Exception {
+ input.user = Strings.nullToEmpty(input.user).trim();
+ if (input.user.isEmpty()) {
+ throw new BadRequestException("missing field: user");
+ }
+ input.reason = Strings.nullToEmpty(input.reason).trim();
+ if (input.reason.isEmpty()) {
+ throw new BadRequestException("missing field: reason");
+ }
+
+ Account.Id attentionUserId = accountResolver.resolve(input.user).asUnique().account().id();
+ try {
+ permissionBackend
+ .absentUser(attentionUserId)
+ .change(changeResource.getNotes())
+ .check(ChangePermission.READ);
+ } catch (AuthException e) {
+ throw new AuthException("read not permitted for " + attentionUserId, e);
+ }
+
+ try (BatchUpdate bu =
+ updateFactory.create(
+ changeResource.getChange().getProject(), changeResource.getUser(), TimeUtil.nowTs())) {
+ AddToAttentionSetOp op = opFactory.create(attentionUserId, input.reason);
+ bu.addOp(changeResource.getId(), op);
+ bu.execute();
+ return Response.ok(accountLoaderFactory.create(true).fillOne(attentionUserId));
+ }
+ }
+}
diff --git a/java/com/google/gerrit/server/restapi/change/AttentionSet.java b/java/com/google/gerrit/server/restapi/change/AttentionSet.java
new file mode 100644
index 0000000..45d78dc
--- /dev/null
+++ b/java/com/google/gerrit/server/restapi/change/AttentionSet.java
@@ -0,0 +1,69 @@
+// Copyright (C) 2020 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.restapi.change;
+
+import com.google.gerrit.entities.Account;
+import com.google.gerrit.extensions.registration.DynamicMap;
+import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.extensions.restapi.ChildCollection;
+import com.google.gerrit.extensions.restapi.IdString;
+import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
+import com.google.gerrit.extensions.restapi.RestView;
+import com.google.gerrit.server.account.AccountResolver;
+import com.google.gerrit.server.account.AccountResolver.UnresolvableAccountException;
+import com.google.gerrit.server.change.AttentionSetEntryResource;
+import com.google.gerrit.server.change.ChangeResource;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+import java.io.IOException;
+import org.eclipse.jgit.errors.ConfigInvalidException;
+
+@Singleton
+public class AttentionSet implements ChildCollection<ChangeResource, AttentionSetEntryResource> {
+ private final DynamicMap<RestView<AttentionSetEntryResource>> views;
+ private final AccountResolver accountResolver;
+ private final GetAttentionSet getAttentionSet;
+
+ @Inject
+ AttentionSet(
+ DynamicMap<RestView<AttentionSetEntryResource>> views,
+ GetAttentionSet getAttentionSet,
+ AccountResolver accountResolver) {
+ this.views = views;
+ this.accountResolver = accountResolver;
+ this.getAttentionSet = getAttentionSet;
+ }
+
+ @Override
+ public DynamicMap<RestView<AttentionSetEntryResource>> views() {
+ return views;
+ }
+
+ @Override
+ public RestView<ChangeResource> list() throws ResourceNotFoundException {
+ return getAttentionSet;
+ }
+
+ @Override
+ public AttentionSetEntryResource parse(ChangeResource changeResource, IdString idString)
+ throws ResourceNotFoundException, AuthException, IOException, ConfigInvalidException {
+ try {
+ Account.Id accountId = accountResolver.resolve(idString.get()).asUnique().account().id();
+ return new AttentionSetEntryResource(changeResource, accountId);
+ } catch (UnresolvableAccountException e) {
+ throw new ResourceNotFoundException(idString, e);
+ }
+ }
+}
diff --git a/java/com/google/gerrit/server/restapi/change/GetAttentionSet.java b/java/com/google/gerrit/server/restapi/change/GetAttentionSet.java
new file mode 100644
index 0000000..5d6d03d
--- /dev/null
+++ b/java/com/google/gerrit/server/restapi/change/GetAttentionSet.java
@@ -0,0 +1,59 @@
+// Copyright (C) 2020 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.restapi.change;
+
+import static com.google.common.collect.ImmutableList.toImmutableList;
+
+import com.google.common.collect.ImmutableList;
+import com.google.gerrit.entities.AttentionSetUpdate.Operation;
+import com.google.gerrit.extensions.common.AttentionSetEntry;
+import com.google.gerrit.extensions.restapi.Response;
+import com.google.gerrit.extensions.restapi.RestReadView;
+import com.google.gerrit.server.account.AccountLoader;
+import com.google.gerrit.server.change.ChangeResource;
+import com.google.gerrit.server.permissions.PermissionBackendException;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+import java.sql.Timestamp;
+import java.util.List;
+
+/** Reads the list of users currently in the attention set. */
+@Singleton
+public class GetAttentionSet implements RestReadView<ChangeResource> {
+
+ private final AccountLoader.Factory accountLoaderFactory;
+
+ @Inject
+ GetAttentionSet(AccountLoader.Factory accountLoaderFactory) {
+ this.accountLoaderFactory = accountLoaderFactory;
+ }
+
+ @Override
+ public Response<List<AttentionSetEntry>> apply(ChangeResource changeResource)
+ throws PermissionBackendException {
+ AccountLoader accountLoader = accountLoaderFactory.create(true);
+ ImmutableList<AttentionSetEntry> response =
+ changeResource.getNotes().getAttentionSet().stream()
+ // This filtering should match ChangeJson.
+ .filter(a -> a.operation() == Operation.ADD)
+ .map(
+ a ->
+ new AttentionSetEntry(
+ accountLoader.get(a.account()), Timestamp.from(a.timestamp()), a.reason()))
+ .collect(toImmutableList());
+ accountLoader.fill();
+ return Response.ok(response);
+ }
+}
diff --git a/java/com/google/gerrit/server/restapi/change/Module.java b/java/com/google/gerrit/server/restapi/change/Module.java
index 453b4df..387d0a8 100644
--- a/java/com/google/gerrit/server/restapi/change/Module.java
+++ b/java/com/google/gerrit/server/restapi/change/Module.java
@@ -14,6 +14,7 @@
package com.google.gerrit.server.restapi.change;
+import static com.google.gerrit.server.change.AttentionSetEntryResource.ATTENTION_SET_ENTRY_KIND;
import static com.google.gerrit.server.change.ChangeEditResource.CHANGE_EDIT_KIND;
import static com.google.gerrit.server.change.ChangeMessageResource.CHANGE_MESSAGE_KIND;
import static com.google.gerrit.server.change.ChangeResource.CHANGE_KIND;
@@ -30,6 +31,7 @@
import com.google.gerrit.extensions.restapi.RestApiModule;
import com.google.gerrit.server.account.AccountLoader;
import com.google.gerrit.server.change.AddReviewersOp;
+import com.google.gerrit.server.change.AddToAttentionSetOp;
import com.google.gerrit.server.change.ChangeInserter;
import com.google.gerrit.server.change.ChangeResource;
import com.google.gerrit.server.change.DeleteChangeOp;
@@ -38,6 +40,7 @@
import com.google.gerrit.server.change.EmailReviewComments;
import com.google.gerrit.server.change.PatchSetInserter;
import com.google.gerrit.server.change.RebaseChangeOp;
+import com.google.gerrit.server.change.RemoveFromAttentionSetOp;
import com.google.gerrit.server.change.ReviewerResource;
import com.google.gerrit.server.change.SetAssigneeOp;
import com.google.gerrit.server.change.SetCherryPickOp;
@@ -72,6 +75,7 @@
DynamicMap.mapOf(binder(), CHANGE_EDIT_KIND);
DynamicMap.mapOf(binder(), VOTE_KIND);
DynamicMap.mapOf(binder(), CHANGE_MESSAGE_KIND);
+ DynamicMap.mapOf(binder(), ATTENTION_SET_ENTRY_KIND);
postOnCollection(CHANGE_KIND).to(CreateChange.class);
get(CHANGE_KIND).to(GetChange.class);
@@ -79,6 +83,10 @@
get(CHANGE_KIND, "detail").to(GetDetail.class);
get(CHANGE_KIND, "topic").to(GetTopic.class);
get(CHANGE_KIND, "in").to(ChangeIncludedIn.class);
+ child(CHANGE_KIND, "attention").to(AttentionSet.class);
+ delete(ATTENTION_SET_ENTRY_KIND).to(RemoveFromAttentionSet.class);
+ post(ATTENTION_SET_ENTRY_KIND, "delete").to(RemoveFromAttentionSet.class);
+ postOnCollection(ATTENTION_SET_ENTRY_KIND).to(AddToAttentionSet.class);
get(CHANGE_KIND, "assignee").to(GetAssignee.class);
get(CHANGE_KIND, "past_assignees").to(GetPastAssignees.class);
put(CHANGE_KIND, "assignee").to(PutAssignee.class);
@@ -207,5 +215,7 @@
factory(SetPrivateOp.Factory.class);
factory(WorkInProgressOp.Factory.class);
factory(SetTopicOp.Factory.class);
+ factory(AddToAttentionSetOp.Factory.class);
+ factory(RemoveFromAttentionSetOp.Factory.class);
}
}
diff --git a/java/com/google/gerrit/server/restapi/change/RemoveFromAttentionSet.java b/java/com/google/gerrit/server/restapi/change/RemoveFromAttentionSet.java
new file mode 100644
index 0000000..ccf375a
--- /dev/null
+++ b/java/com/google/gerrit/server/restapi/change/RemoveFromAttentionSet.java
@@ -0,0 +1,70 @@
+// Copyright (C) 2020 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.restapi.change;
+
+import com.google.common.base.Strings;
+import com.google.gerrit.extensions.api.changes.RemoveFromAttentionSetInput;
+import com.google.gerrit.extensions.restapi.BadRequestException;
+import com.google.gerrit.extensions.restapi.Response;
+import com.google.gerrit.extensions.restapi.RestApiException;
+import com.google.gerrit.extensions.restapi.RestModifyView;
+import com.google.gerrit.server.change.AttentionSetEntryResource;
+import com.google.gerrit.server.change.ChangeResource;
+import com.google.gerrit.server.change.RemoveFromAttentionSetOp;
+import com.google.gerrit.server.permissions.PermissionBackendException;
+import com.google.gerrit.server.update.BatchUpdate;
+import com.google.gerrit.server.update.UpdateException;
+import com.google.gerrit.server.util.time.TimeUtil;
+import com.google.inject.Inject;
+import java.io.IOException;
+import org.eclipse.jgit.errors.ConfigInvalidException;
+
+/** Removes a single user from the attention set. */
+public class RemoveFromAttentionSet
+ implements RestModifyView<AttentionSetEntryResource, RemoveFromAttentionSetInput> {
+ private final BatchUpdate.Factory updateFactory;
+ private final RemoveFromAttentionSetOp.Factory opFactory;
+
+ @Inject
+ RemoveFromAttentionSet(
+ BatchUpdate.Factory updateFactory, RemoveFromAttentionSetOp.Factory opFactory) {
+ this.updateFactory = updateFactory;
+ this.opFactory = opFactory;
+ }
+
+ @Override
+ public Response<Object> apply(
+ AttentionSetEntryResource attentionResource, RemoveFromAttentionSetInput input)
+ throws RestApiException, PermissionBackendException, IOException, ConfigInvalidException,
+ UpdateException {
+ if (input == null) {
+ throw new BadRequestException("input may not be null");
+ }
+ input.reason = Strings.nullToEmpty(input.reason).trim();
+ if (input.reason.isEmpty()) {
+ throw new BadRequestException("missing field: reason");
+ }
+ ChangeResource changeResource = attentionResource.getChangeResource();
+ try (BatchUpdate bu =
+ updateFactory.create(
+ changeResource.getProject(), changeResource.getUser(), TimeUtil.nowTs())) {
+ RemoveFromAttentionSetOp op =
+ opFactory.create(attentionResource.getAccountId(), input.reason);
+ bu.addOp(changeResource.getId(), op);
+ bu.execute();
+ }
+ return Response.none();
+ }
+}
diff --git a/javatests/com/google/gerrit/acceptance/api/project/CheckProjectIT.java b/javatests/com/google/gerrit/acceptance/api/project/CheckProjectIT.java
index 27dd16a..4163e17 100644
--- a/javatests/com/google/gerrit/acceptance/api/project/CheckProjectIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/project/CheckProjectIT.java
@@ -53,14 +53,14 @@
PushOneCommit.Result r = createChange("refs/for/master");
String branch = r.getChange().change().getDest().branch();
- ChangeInfo info = gApi.changes().id(r.getChange().getId().get()).info();
+ ChangeInfo info = change(r).info();
assertThat(info.status).isEqualTo(ChangeStatus.NEW);
CheckProjectResultInfo checkResult =
gApi.projects().name(project.get()).check(checkProjectInputForAutoCloseableCheck(branch));
assertThat(checkResult.autoCloseableChangesCheckResult.autoCloseableChanges).isEmpty();
- info = gApi.changes().id(r.getChange().getId().get()).info();
+ info = change(r).info();
assertThat(info.status).isEqualTo(ChangeStatus.NEW);
}
@@ -121,7 +121,7 @@
RevCommit amendedCommit = serverSideTestRepo.amend(r.getCommit()).create();
serverSideTestRepo.branch(branch).update(amendedCommit);
- ChangeInfo info = gApi.changes().id(r.getChange().getId().get()).info();
+ ChangeInfo info = change(r).info();
assertThat(info.status).isEqualTo(ChangeStatus.NEW);
CheckProjectResultInfo checkResult =
@@ -132,7 +132,7 @@
.collect(toSet()))
.containsExactly(r.getChange().getId().get());
- info = gApi.changes().id(r.getChange().getId().get()).info();
+ info = change(r).info();
assertThat(info.status).isEqualTo(ChangeStatus.NEW);
}
@@ -144,7 +144,7 @@
RevCommit amendedCommit = serverSideTestRepo.amend(r.getCommit()).create();
serverSideTestRepo.branch(branch).update(amendedCommit);
- ChangeInfo info = gApi.changes().id(r.getChange().getId().get()).info();
+ ChangeInfo info = change(r).info();
assertThat(info.status).isEqualTo(ChangeStatus.NEW);
CheckProjectInput input = checkProjectInputForAutoCloseableCheck(branch);
@@ -156,7 +156,7 @@
.collect(toSet()))
.containsExactly(r.getChange().getId().get());
- info = gApi.changes().id(r.getChange().getId().get()).info();
+ info = change(r).info();
assertThat(info.status).isEqualTo(ChangeStatus.MERGED);
}
@@ -170,7 +170,7 @@
serverSideTestRepo.commit(amendedCommit);
- ChangeInfo info = gApi.changes().id(r.getChange().getId().get()).info();
+ ChangeInfo info = change(r).info();
assertThat(info.status).isEqualTo(ChangeStatus.NEW);
CheckProjectInput input = checkProjectInputForAutoCloseableCheck(branch);
@@ -179,7 +179,7 @@
CheckProjectResultInfo checkResult = gApi.projects().name(project.get()).check(input);
assertThat(checkResult.autoCloseableChangesCheckResult.autoCloseableChanges).isEmpty();
- info = gApi.changes().id(r.getChange().getId().get()).info();
+ info = change(r).info();
assertThat(info.status).isEqualTo(ChangeStatus.NEW);
input.autoCloseableChangesCheck.maxCommits = 2;
@@ -190,7 +190,7 @@
.collect(toSet()))
.containsExactly(r.getChange().getId().get());
- info = gApi.changes().id(r.getChange().getId().get()).info();
+ info = change(r).info();
assertThat(info.status).isEqualTo(ChangeStatus.MERGED);
}
@@ -204,7 +204,7 @@
serverSideTestRepo.commit(amendedCommit);
- ChangeInfo info = gApi.changes().id(r.getChange().getId().get()).info();
+ ChangeInfo info = change(r).info();
assertThat(info.status).isEqualTo(ChangeStatus.NEW);
CheckProjectInput input = checkProjectInputForAutoCloseableCheck(branch);
@@ -213,7 +213,7 @@
CheckProjectResultInfo checkResult = gApi.projects().name(project.get()).check(input);
assertThat(checkResult.autoCloseableChangesCheckResult.autoCloseableChanges).isEmpty();
- info = gApi.changes().id(r.getChange().getId().get()).info();
+ info = change(r).info();
assertThat(info.status).isEqualTo(ChangeStatus.NEW);
input.autoCloseableChangesCheck.skipCommits = 1;
@@ -224,7 +224,7 @@
.collect(toSet()))
.containsExactly(r.getChange().getId().get());
- info = gApi.changes().id(r.getChange().getId().get()).info();
+ info = change(r).info();
assertThat(info.status).isEqualTo(ChangeStatus.MERGED);
}
diff --git a/javatests/com/google/gerrit/acceptance/api/revision/RevisionIT.java b/javatests/com/google/gerrit/acceptance/api/revision/RevisionIT.java
index 70fcfc4..2c2abfe 100644
--- a/javatests/com/google/gerrit/acceptance/api/revision/RevisionIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/revision/RevisionIT.java
@@ -303,13 +303,7 @@
PushOneCommit.Result r = createChange();
requestScopeOperations.setApiUser(user.id());
AuthException thrown =
- assertThrows(
- AuthException.class,
- () ->
- gApi.changes()
- .id(r.getChange().getId().get())
- .current()
- .review(ReviewInput.approve()));
+ assertThrows(AuthException.class, () -> change(r).current().review(ReviewInput.approve()));
assertThat(thrown).hasMessageThat().contains("is restricted");
}
@@ -560,7 +554,7 @@
PushOneCommit.Result r = push.to("refs/for/master%topic=someTopic");
// Verify before the cherry-pick that the change has exactly 1 message.
- ChangeApi changeApi = gApi.changes().id(r.getChange().getId().get());
+ ChangeApi changeApi = change(r);
assertThat(changeApi.get().messages).hasSize(1);
// Cherry-pick the change to the other branch, that should fail with a conflict.
diff --git a/javatests/com/google/gerrit/acceptance/git/AbstractPushForReview.java b/javatests/com/google/gerrit/acceptance/git/AbstractPushForReview.java
index f190d59..d1e8cc5 100644
--- a/javatests/com/google/gerrit/acceptance/git/AbstractPushForReview.java
+++ b/javatests/com/google/gerrit/acceptance/git/AbstractPushForReview.java
@@ -437,7 +437,7 @@
r.assertErrorStatus("change " + url + " closed");
// Check change message that was added on auto-close
- ChangeInfo change = gApi.changes().id(r.getChange().getId().get()).get();
+ ChangeInfo change = change(r).get();
assertThat(Iterables.getLast(change.messages).message)
.isEqualTo("Change has been successfully pushed.");
}
@@ -477,7 +477,7 @@
r.assertErrorStatus("change " + url + " closed");
// Check that new commit was added as patch set
- ChangeInfo change = gApi.changes().id(r.getChange().getId().get()).get();
+ ChangeInfo change = change(r).get();
assertThat(change.revisions).hasSize(2);
assertThat(change.currentRevision).isEqualTo(c.name());
}
diff --git a/javatests/com/google/gerrit/acceptance/rest/binding/ChangesRestApiBindingsIT.java b/javatests/com/google/gerrit/acceptance/rest/binding/ChangesRestApiBindingsIT.java
index 83bc3eb..574e919 100644
--- a/javatests/com/google/gerrit/acceptance/rest/binding/ChangesRestApiBindingsIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/binding/ChangesRestApiBindingsIT.java
@@ -63,6 +63,8 @@
RestCall.get("/changes/%s/comments"),
RestCall.get("/changes/%s/robotcomments"),
RestCall.get("/changes/%s/drafts"),
+ RestCall.get("/changes/%s/attention"),
+ RestCall.post("/changes/%s/attention"),
RestCall.get("/changes/%s/assignee"),
RestCall.get("/changes/%s/past_assignees"),
RestCall.put("/changes/%s/assignee"),
@@ -267,6 +269,11 @@
// Delete content of a file in an existing change edit.
RestCall.delete("/changes/%s/edit/%s"));
+ private static final ImmutableList<RestCall> ATTENTION_SET_ENDPOINTS =
+ ImmutableList.of(
+ RestCall.post("/changes/%s/attention/%s/delete"),
+ RestCall.delete("/changes/%s/attention/%s"));
+
private static final String FILENAME = "test.txt";
@Test
@@ -477,6 +484,14 @@
RestApiCallHelper.execute(adminRestSession, CHANGE_EDIT_ENDPOINTS, changeId, FILENAME);
}
+ @Test
+ public void attentionSetEndpoints() throws Exception {
+ String changeId = createChange().getChangeId();
+ gApi.changes().id(changeId).edit().create();
+ RestApiCallHelper.execute(
+ adminRestSession, ATTENTION_SET_ENDPOINTS, changeId, user.id().toString());
+ }
+
private static Comment.Range createRange(
int startLine, int startCharacter, int endLine, int endCharacter) {
Comment.Range range = new Comment.Range();
diff --git a/javatests/com/google/gerrit/acceptance/rest/change/AssigneeIT.java b/javatests/com/google/gerrit/acceptance/rest/change/AssigneeIT.java
index 420ddda..2d47dd8 100644
--- a/javatests/com/google/gerrit/acceptance/rest/change/AssigneeIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/change/AssigneeIT.java
@@ -188,11 +188,11 @@
}
private AccountInfo getAssignee(PushOneCommit.Result r) throws Exception {
- return gApi.changes().id(r.getChange().getId().get()).getAssignee();
+ return change(r).getAssignee();
}
private List<AccountInfo> getPastAssignees(PushOneCommit.Result r) throws Exception {
- return gApi.changes().id(r.getChange().getId().get()).getPastAssignees();
+ return change(r).getPastAssignees();
}
private Iterable<AccountInfo> getReviewers(PushOneCommit.Result r, ReviewerState state)
@@ -203,10 +203,10 @@
private AccountInfo setAssignee(PushOneCommit.Result r, String identifieer) throws Exception {
AssigneeInput input = new AssigneeInput();
input.assignee = identifieer;
- return gApi.changes().id(r.getChange().getId().get()).setAssignee(input);
+ return change(r).setAssignee(input);
}
private AccountInfo deleteAssignee(PushOneCommit.Result r) throws Exception {
- return gApi.changes().id(r.getChange().getId().get()).deleteAssignee();
+ return change(r).deleteAssignee();
}
}
diff --git a/javatests/com/google/gerrit/acceptance/rest/change/AttentionSetIT.java b/javatests/com/google/gerrit/acceptance/rest/change/AttentionSetIT.java
new file mode 100644
index 0000000..caa8832
--- /dev/null
+++ b/javatests/com/google/gerrit/acceptance/rest/change/AttentionSetIT.java
@@ -0,0 +1,136 @@
+// 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.rest.change;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.gerrit.acceptance.AbstractDaemonTest;
+import com.google.gerrit.acceptance.NoHttpd;
+import com.google.gerrit.acceptance.PushOneCommit;
+import com.google.gerrit.acceptance.UseClockStep;
+import com.google.gerrit.entities.AttentionSetUpdate;
+import com.google.gerrit.extensions.api.changes.AddToAttentionSetInput;
+import com.google.gerrit.extensions.api.changes.RemoveFromAttentionSetInput;
+import com.google.gerrit.server.util.time.TimeUtil;
+import java.time.Duration;
+import java.time.Instant;
+import java.util.concurrent.TimeUnit;
+import java.util.function.LongSupplier;
+import org.junit.Before;
+import org.junit.Test;
+
+@NoHttpd
+@UseClockStep(clockStepUnit = TimeUnit.MINUTES)
+public class AttentionSetIT extends AbstractDaemonTest {
+ /** Simulates a fake clock. Uses second granularity. */
+ private static class FakeClock implements LongSupplier {
+ Instant now = Instant.now();
+
+ @Override
+ public long getAsLong() {
+ return TimeUnit.SECONDS.toMillis(now.getEpochSecond());
+ }
+
+ Instant now() {
+ return Instant.ofEpochSecond(now.getEpochSecond());
+ }
+
+ void advance(Duration duration) {
+ now = now.plus(duration);
+ }
+ }
+
+ private FakeClock fakeClock = new FakeClock();
+
+ @Before
+ public void setUp() {
+ TimeUtil.setCurrentMillisSupplier(fakeClock);
+ }
+
+ @Test
+ public void emptyAttentionSet() throws Exception {
+ PushOneCommit.Result r = createChange();
+ assertThat(r.getChange().attentionSet()).isEmpty();
+ }
+
+ @Test
+ public void addUser() throws Exception {
+ PushOneCommit.Result r = createChange();
+ int accountId =
+ change(r).addToAttentionSet(new AddToAttentionSetInput(user.email(), "first"))._accountId;
+ assertThat(accountId).isEqualTo(user.id().get());
+ AttentionSetUpdate expectedAttentionSetUpdate =
+ AttentionSetUpdate.createFromRead(
+ fakeClock.now(), user.id(), AttentionSetUpdate.Operation.ADD, "first");
+ assertThat(r.getChange().attentionSet()).containsExactly(expectedAttentionSetUpdate);
+
+ // Second add is ignored.
+ accountId =
+ change(r).addToAttentionSet(new AddToAttentionSetInput(user.email(), "second"))._accountId;
+ assertThat(accountId).isEqualTo(user.id().get());
+ assertThat(r.getChange().attentionSet()).containsExactly(expectedAttentionSetUpdate);
+ }
+
+ @Test
+ public void addMultipleUsers() throws Exception {
+ PushOneCommit.Result r = createChange();
+ Instant timestamp1 = fakeClock.now();
+ int accountId1 =
+ change(r).addToAttentionSet(new AddToAttentionSetInput(user.email(), "user"))._accountId;
+ assertThat(accountId1).isEqualTo(user.id().get());
+ fakeClock.advance(Duration.ofSeconds(42));
+ Instant timestamp2 = fakeClock.now();
+ int accountId2 =
+ change(r)
+ .addToAttentionSet(new AddToAttentionSetInput(admin.id().toString(), "admin"))
+ ._accountId;
+ assertThat(accountId2).isEqualTo(admin.id().get());
+
+ AttentionSetUpdate expectedAttentionSetUpdate1 =
+ AttentionSetUpdate.createFromRead(
+ timestamp1, user.id(), AttentionSetUpdate.Operation.ADD, "user");
+ AttentionSetUpdate expectedAttentionSetUpdate2 =
+ AttentionSetUpdate.createFromRead(
+ timestamp2, admin.id(), AttentionSetUpdate.Operation.ADD, "admin");
+ assertThat(r.getChange().attentionSet())
+ .containsExactly(expectedAttentionSetUpdate1, expectedAttentionSetUpdate2);
+ }
+
+ @Test
+ public void removeUser() throws Exception {
+ PushOneCommit.Result r = createChange();
+ change(r).addToAttentionSet(new AddToAttentionSetInput(user.email(), "added"));
+ fakeClock.advance(Duration.ofSeconds(42));
+ change(r).attention(user.id().toString()).remove(new RemoveFromAttentionSetInput("removed"));
+ AttentionSetUpdate expectedAttentionSetUpdate =
+ AttentionSetUpdate.createFromRead(
+ fakeClock.now(), user.id(), AttentionSetUpdate.Operation.REMOVE, "removed");
+ assertThat(r.getChange().attentionSet()).containsExactly(expectedAttentionSetUpdate);
+
+ // Second removal is ignored.
+ fakeClock.advance(Duration.ofSeconds(42));
+ change(r)
+ .attention(user.id().toString())
+ .remove(new RemoveFromAttentionSetInput("removed again"));
+ assertThat(r.getChange().attentionSet()).containsExactly(expectedAttentionSetUpdate);
+ }
+
+ @Test
+ public void removeUnrelatedUser() throws Exception {
+ PushOneCommit.Result r = createChange();
+ change(r).attention(user.id().toString()).remove(new RemoveFromAttentionSetInput("foo"));
+ assertThat(r.getChange().attentionSet()).isEmpty();
+ }
+}
diff --git a/javatests/com/google/gerrit/acceptance/rest/change/HashtagsIT.java b/javatests/com/google/gerrit/acceptance/rest/change/HashtagsIT.java
index c57a035..0099fe6 100644
--- a/javatests/com/google/gerrit/acceptance/rest/change/HashtagsIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/change/HashtagsIT.java
@@ -226,7 +226,7 @@
HashtagsInput input = new HashtagsInput();
input.add = Sets.newHashSet("tag3", "tag4");
input.remove = Sets.newHashSet("tag1");
- gApi.changes().id(r.getChange().getId().get()).setHashtags(input);
+ change(r).setHashtags(input);
assertThatGet(r).containsExactly("tag2", "tag3", "tag4");
assertMessage(r, "Hashtags added: tag3, tag4\nHashtag removed: tag1");
@@ -235,7 +235,7 @@
input = new HashtagsInput();
input.add = Sets.newHashSet("tag3", "tag4");
input.remove = Sets.newHashSet("tag3");
- gApi.changes().id(r.getChange().getId().get()).setHashtags(input);
+ change(r).setHashtags(input);
assertThatGet(r).containsExactly("tag1", "tag2", "tag4");
assertMessage(r, "Hashtag removed: tag3");
}
@@ -271,19 +271,19 @@
}
private IterableSubject assertThatGet(PushOneCommit.Result r) throws Exception {
- return assertThat(gApi.changes().id(r.getChange().getId().get()).getHashtags());
+ return assertThat(change(r).getHashtags());
}
private void addHashtags(PushOneCommit.Result r, String... toAdd) throws Exception {
HashtagsInput input = new HashtagsInput();
input.add = Sets.newHashSet(toAdd);
- gApi.changes().id(r.getChange().getId().get()).setHashtags(input);
+ change(r).setHashtags(input);
}
private void removeHashtags(PushOneCommit.Result r, String... toRemove) throws Exception {
HashtagsInput input = new HashtagsInput();
input.remove = Sets.newHashSet(toRemove);
- gApi.changes().id(r.getChange().getId().get()).setHashtags(input);
+ change(r).setHashtags(input);
}
private void assertMessage(PushOneCommit.Result r, String expectedMessage) throws Exception {
@@ -299,8 +299,7 @@
}
private ChangeMessageInfo getLastMessage(PushOneCommit.Result r) throws Exception {
- ChangeMessageInfo lastMessage =
- Iterables.getLast(gApi.changes().id(r.getChange().getId().get()).get().messages, null);
+ ChangeMessageInfo lastMessage = Iterables.getLast(change(r).get().messages, null);
assertWithMessage(lastMessage.message).that(lastMessage).isNotNull();
return lastMessage;
}
diff --git a/javatests/com/google/gerrit/server/notedb/ChangeNotesStateTest.java b/javatests/com/google/gerrit/server/notedb/ChangeNotesStateTest.java
index 74fee65..0674dc0 100644
--- a/javatests/com/google/gerrit/server/notedb/ChangeNotesStateTest.java
+++ b/javatests/com/google/gerrit/server/notedb/ChangeNotesStateTest.java
@@ -28,7 +28,7 @@
import com.google.gerrit.common.data.SubmitRecord;
import com.google.gerrit.common.data.SubmitRequirement;
import com.google.gerrit.entities.Account;
-import com.google.gerrit.entities.AttentionStatus;
+import com.google.gerrit.entities.AttentionSetUpdate;
import com.google.gerrit.entities.Change;
import com.google.gerrit.entities.ChangeMessage;
import com.google.gerrit.entities.Comment;
@@ -45,7 +45,7 @@
import com.google.gerrit.server.ReviewerStatusUpdate;
import com.google.gerrit.server.cache.proto.Cache.ChangeNotesStateProto;
import com.google.gerrit.server.cache.proto.Cache.ChangeNotesStateProto.AssigneeStatusUpdateProto;
-import com.google.gerrit.server.cache.proto.Cache.ChangeNotesStateProto.AttentionStatusProto;
+import com.google.gerrit.server.cache.proto.Cache.ChangeNotesStateProto.AttentionSetUpdateProto;
import com.google.gerrit.server.cache.proto.Cache.ChangeNotesStateProto.ChangeColumnsProto;
import com.google.gerrit.server.cache.proto.Cache.ChangeNotesStateProto.ReviewerByEmailSetEntryProto;
import com.google.gerrit.server.cache.proto.Cache.ChangeNotesStateProto.ReviewerSetEntryProto;
@@ -600,31 +600,31 @@
}
@Test
- public void serializeAttentionUpdates() throws Exception {
+ public void serializeAttentionSetUpdates() throws Exception {
assertRoundTrip(
newBuilder()
- .attentionUpdates(
+ .attentionSet(
ImmutableList.of(
- AttentionStatus.createFromRead(
+ AttentionSetUpdate.createFromRead(
Instant.EPOCH.plusSeconds(23),
Account.id(1000),
- AttentionStatus.Operation.ADD,
+ AttentionSetUpdate.Operation.ADD,
"reason 1"),
- AttentionStatus.createFromRead(
+ AttentionSetUpdate.createFromRead(
Instant.EPOCH.plusSeconds(42),
Account.id(2000),
- AttentionStatus.Operation.REMOVE,
+ AttentionSetUpdate.Operation.REMOVE,
"reason 2")))
.build(),
newProtoBuilder()
- .addAttentionStatus(
- AttentionStatusProto.newBuilder()
+ .addAttentionSetUpdate(
+ AttentionSetUpdateProto.newBuilder()
.setTimestampMillis(23_000) // epoch millis
.setAccount(1000)
.setOperation("ADD")
.setReason("reason 1"))
- .addAttentionStatus(
- AttentionStatusProto.newBuilder()
+ .addAttentionSetUpdate(
+ AttentionSetUpdateProto.newBuilder()
.setTimestampMillis(42_000) // epoch millis
.setAccount(2000)
.setOperation("REMOVE")
@@ -789,8 +789,8 @@
"reviewerUpdates",
new TypeLiteral<ImmutableList<ReviewerStatusUpdate>>() {}.getType())
.put(
- "attentionUpdates",
- new TypeLiteral<ImmutableList<AttentionStatus>>() {}.getType())
+ "attentionSet",
+ new TypeLiteral<ImmutableList<AttentionSetUpdate>>() {}.getType())
.put(
"assigneeUpdates",
new TypeLiteral<ImmutableList<AssigneeStatusUpdate>>() {}.getType())
diff --git a/javatests/com/google/gerrit/server/notedb/ChangeNotesTest.java b/javatests/com/google/gerrit/server/notedb/ChangeNotesTest.java
index 4e068ba..f5a6dc3 100644
--- a/javatests/com/google/gerrit/server/notedb/ChangeNotesTest.java
+++ b/javatests/com/google/gerrit/server/notedb/ChangeNotesTest.java
@@ -38,8 +38,8 @@
import com.google.common.collect.Lists;
import com.google.gerrit.common.data.SubmitRecord;
import com.google.gerrit.entities.Account;
-import com.google.gerrit.entities.AttentionStatus;
-import com.google.gerrit.entities.AttentionStatus.Operation;
+import com.google.gerrit.entities.AttentionSetUpdate;
+import com.google.gerrit.entities.AttentionSetUpdate.Operation;
import com.google.gerrit.entities.BranchNameKey;
import com.google.gerrit.entities.Change;
import com.google.gerrit.entities.ChangeMessage;
@@ -694,51 +694,51 @@
public void defaultAttentionSetIsEmpty() throws Exception {
Change c = newChange();
ChangeNotes notes = newNotes(c);
- assertThat(notes.getAttentionUpdates()).isEmpty();
+ assertThat(notes.getAttentionSet()).isEmpty();
}
@Test
public void addAttentionStatus() throws Exception {
Change c = newChange();
ChangeUpdate update = newUpdate(c, changeOwner);
- AttentionStatus attentionStatus =
- AttentionStatus.createForWrite(changeOwner.getAccountId(), Operation.ADD, "test");
- update.setAttentionUpdates(ImmutableList.of(attentionStatus));
+ AttentionSetUpdate attentionSetUpdate =
+ AttentionSetUpdate.createForWrite(changeOwner.getAccountId(), Operation.ADD, "test");
+ update.setAttentionSetUpdates(ImmutableList.of(attentionSetUpdate));
update.commit();
ChangeNotes notes = newNotes(c);
- assertThat(notes.getAttentionUpdates()).containsExactly(addTimestamp(attentionStatus, c));
+ assertThat(notes.getAttentionSet()).containsExactly(addTimestamp(attentionSetUpdate, c));
}
@Test
public void filterLatestAttentionStatus() throws Exception {
Change c = newChange();
ChangeUpdate update = newUpdate(c, changeOwner);
- AttentionStatus attentionStatus =
- AttentionStatus.createForWrite(changeOwner.getAccountId(), Operation.ADD, "test");
- update.setAttentionUpdates(ImmutableList.of(attentionStatus));
+ AttentionSetUpdate attentionSetUpdate =
+ AttentionSetUpdate.createForWrite(changeOwner.getAccountId(), Operation.ADD, "test");
+ update.setAttentionSetUpdates(ImmutableList.of(attentionSetUpdate));
update.commit();
update = newUpdate(c, changeOwner);
- attentionStatus =
- AttentionStatus.createForWrite(changeOwner.getAccountId(), Operation.REMOVE, "test");
- update.setAttentionUpdates(ImmutableList.of(attentionStatus));
+ attentionSetUpdate =
+ AttentionSetUpdate.createForWrite(changeOwner.getAccountId(), Operation.REMOVE, "test");
+ update.setAttentionSetUpdates(ImmutableList.of(attentionSetUpdate));
update.commit();
ChangeNotes notes = newNotes(c);
- assertThat(notes.getAttentionUpdates()).containsExactly(addTimestamp(attentionStatus, c));
+ assertThat(notes.getAttentionSet()).containsExactly(addTimestamp(attentionSetUpdate, c));
}
@Test
public void addAttentionStatus_rejectTimestamp() throws Exception {
Change c = newChange();
ChangeUpdate update = newUpdate(c, changeOwner);
- AttentionStatus attentionStatus =
- AttentionStatus.createFromRead(
+ AttentionSetUpdate attentionSetUpdate =
+ AttentionSetUpdate.createFromRead(
Instant.now(), changeOwner.getAccountId(), Operation.ADD, "test");
IllegalArgumentException thrown =
assertThrows(
IllegalArgumentException.class,
- () -> update.setAttentionUpdates(ImmutableList.of(attentionStatus)));
+ () -> update.setAttentionSetUpdates(ImmutableList.of(attentionSetUpdate)));
assertThat(thrown).hasMessageThat().contains("must not specify timestamp for write");
}
@@ -746,14 +746,16 @@
public void addAttentionStatus_rejectMultiplePerUser() throws Exception {
Change c = newChange();
ChangeUpdate update = newUpdate(c, changeOwner);
- AttentionStatus attentionStatus0 =
- AttentionStatus.createForWrite(changeOwner.getAccountId(), Operation.ADD, "test 0");
- AttentionStatus attentionStatus1 =
- AttentionStatus.createForWrite(changeOwner.getAccountId(), Operation.ADD, "test 1");
+ AttentionSetUpdate attentionSetUpdate0 =
+ AttentionSetUpdate.createForWrite(changeOwner.getAccountId(), Operation.ADD, "test 0");
+ AttentionSetUpdate attentionSetUpdate1 =
+ AttentionSetUpdate.createForWrite(changeOwner.getAccountId(), Operation.ADD, "test 1");
IllegalArgumentException thrown =
assertThrows(
IllegalArgumentException.class,
- () -> update.setAttentionUpdates(ImmutableList.of(attentionStatus0, attentionStatus1)));
+ () ->
+ update.setAttentionSetUpdates(
+ ImmutableList.of(attentionSetUpdate0, attentionSetUpdate1)));
assertThat(thrown)
.hasMessageThat()
.contains("must not specify multiple updates for single user");
@@ -763,17 +765,18 @@
public void addAttentionStatusForMultipleUsers() throws Exception {
Change c = newChange();
ChangeUpdate update = newUpdate(c, changeOwner);
- AttentionStatus attentionStatus0 =
- AttentionStatus.createForWrite(changeOwner.getAccountId(), Operation.ADD, "test");
- AttentionStatus attentionStatus1 =
- AttentionStatus.createForWrite(otherUser.getAccountId(), Operation.ADD, "test");
+ AttentionSetUpdate attentionSetUpdate0 =
+ AttentionSetUpdate.createForWrite(changeOwner.getAccountId(), Operation.ADD, "test");
+ AttentionSetUpdate attentionSetUpdate1 =
+ AttentionSetUpdate.createForWrite(otherUser.getAccountId(), Operation.ADD, "test");
- update.setAttentionUpdates(ImmutableList.of(attentionStatus0, attentionStatus1));
+ update.setAttentionSetUpdates(ImmutableList.of(attentionSetUpdate0, attentionSetUpdate1));
update.commit();
ChangeNotes notes = newNotes(c);
- assertThat(notes.getAttentionUpdates())
- .containsExactly(addTimestamp(attentionStatus0, c), addTimestamp(attentionStatus1, c));
+ assertThat(notes.getAttentionSet())
+ .containsExactly(
+ addTimestamp(attentionSetUpdate0, c), addTimestamp(attentionSetUpdate1, c));
}
@Test
@@ -3230,12 +3233,12 @@
return tr.parseBody(commit);
}
- private AttentionStatus addTimestamp(AttentionStatus attentionStatus, Change c) {
+ private AttentionSetUpdate addTimestamp(AttentionSetUpdate attentionSetUpdate, Change c) {
Timestamp timestamp = newNotes(c).getChange().getLastUpdatedOn();
- return AttentionStatus.createFromRead(
+ return AttentionSetUpdate.createFromRead(
timestamp.toInstant(),
- attentionStatus.account(),
- attentionStatus.operation(),
- attentionStatus.reason());
+ attentionSetUpdate.account(),
+ attentionSetUpdate.operation(),
+ attentionSetUpdate.reason());
}
}
diff --git a/polygerrit-ui/app/behaviors/gr-display-name-behavior/gr-display-name-behavior.js b/polygerrit-ui/app/behaviors/gr-display-name-behavior/gr-display-name-behavior.js
index 1ba8fd9..2e7f5d4 100644
--- a/polygerrit-ui/app/behaviors/gr-display-name-behavior/gr-display-name-behavior.js
+++ b/polygerrit-ui/app/behaviors/gr-display-name-behavior/gr-display-name-behavior.js
@@ -25,12 +25,8 @@
Gerrit.DisplayNameBehavior = {
// TODO(dmfilippov) replace DisplayNameBehavior with GrDisplayNameUtils
- /**
- * enableEmail when true enables to fallback to using email if
- * the account name is not avilable.
- */
- getUserName(config, account, enableEmail) {
- return GrDisplayNameUtils.getUserName(config, account, enableEmail);
+ getUserName(config, account) {
+ return GrDisplayNameUtils.getUserName(config, account);
},
getGroupDisplayName(group) {
diff --git a/polygerrit-ui/app/behaviors/gr-display-name-behavior/gr-display-name-behavior_test.html b/polygerrit-ui/app/behaviors/gr-display-name-behavior/gr-display-name-behavior_test.html
index 863f708..26022c4 100644
--- a/polygerrit-ui/app/behaviors/gr-display-name-behavior/gr-display-name-behavior_test.html
+++ b/polygerrit-ui/app/behaviors/gr-display-name-behavior/gr-display-name-behavior_test.html
@@ -65,26 +65,26 @@
const account = {
name: 'test-name',
};
- assert.deepEqual(element.getUserName(config, account, true), 'test-name');
+ assert.equal(element.getUserName(config, account), 'test-name');
});
test('getUserName username only', () => {
const account = {
username: 'test-user',
};
- assert.deepEqual(element.getUserName(config, account, true), 'test-user');
+ assert.equal(element.getUserName(config, account), 'test-user');
});
test('getUserName email only', () => {
const account = {
email: 'test-user@test-url.com',
};
- assert.deepEqual(element.getUserName(config, account, true),
+ assert.equal(element.getUserName(config, account),
'test-user@test-url.com');
});
test('getUserName returns not Anonymous Coward as the anon name', () => {
- assert.deepEqual(element.getUserName(config, null, true), 'Anonymous');
+ assert.equal(element.getUserName(config, null), 'Anonymous');
});
test('getUserName for the config returning the anon name', () => {
@@ -93,7 +93,7 @@
anonymous_coward_name: 'Test Anon',
},
};
- assert.deepEqual(element.getUserName(config, null, true), 'Test Anon');
+ assert.equal(element.getUserName(config, null), 'Test Anon');
});
test('getGroupDisplayName', () => {
diff --git a/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.js b/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.js
index 51888d6..2b4e158 100644
--- a/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.js
+++ b/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.js
@@ -969,7 +969,8 @@
}
showRevertDialog() {
- const query = 'submissionid:' + this.change.submission_id;
+ // The search is still broken if there is a " in the topic.
+ const query = `submissionid: "${this.change.submission_id}"`;
/* A chromium plugin expects that the modifyRevertMsg hook will only
be called after the revert button is pressed, hence we populate the
revert dialog after revert button is pressed. */
diff --git a/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions_test.html b/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions_test.html
index 0d78fb4..7a074a9 100644
--- a/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions_test.html
+++ b/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions_test.html
@@ -921,12 +921,13 @@
});
suite('revert change submitted together', () => {
+ let getChangesStub;
setup(() => {
element.change = {
- submission_id: '199',
+ submission_id: '199 0',
current_revision: '2000',
};
- sandbox.stub(element.$.restAPI, 'getChanges')
+ getChangesStub = sandbox.stub(element.$.restAPI, 'getChanges')
.returns(Promise.resolve([
{change_id: '12345678901234', topic: 'T', subject: 'random'},
{change_id: '23456', topic: 'T', subject: 'a'.repeat(100)},
@@ -938,6 +939,7 @@
.querySelector('gr-button[data-action-key="revert"]');
MockInteractions.tap(revertButton);
flush(() => {
+ assert.equal(getChangesStub.args[0][1], 'submissionid: "199 0"');
const confirmRevertDialog = element.$.confirmRevertDialog;
const revertSingleChangeLabel = confirmRevertDialog
.shadowRoot.querySelector('.revertSingleChange');
@@ -947,7 +949,7 @@
'Revert single change');
assert(revertSubmissionLabel.innerText.trim() ===
'Revert entire submission (2 Changes)');
- let expectedMsg = 'Revert submission 199' + '\n\n' +
+ let expectedMsg = 'Revert submission 199 0' + '\n\n' +
'Reason for revert: <INSERT REASONING HERE>' + '\n' +
'Reverted Changes:' + '\n' +
'1234567890:random' + '\n' +
@@ -994,7 +996,7 @@
flush(() => {
const radioInputs = confirmRevertDialog.shadowRoot
.querySelectorAll('input[name="revertOptions"]');
- const revertSubmissionMsg = 'Revert submission 199' + '\n\n' +
+ const revertSubmissionMsg = 'Revert submission 199 0' + '\n\n' +
'Reason for revert: <INSERT REASONING HERE>' + '\n' +
'Reverted Changes:' + '\n' +
'1234567890:random' + '\n' +
diff --git a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.js b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.js
index 05afa35..b951237 100644
--- a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.js
+++ b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.js
@@ -1837,8 +1837,9 @@
* @param {!Object} change
*/
_computeCopyTextForTitle(change) {
- return `${change._number}: ${change.subject}` +
- ` | https://${location.host}${this._computeChangeUrl(change)}`;
+ return `${change._number}: ${change.subject} | ` +
+ `${location.protocol}//${location.host}` +
+ `${this._computeChangeUrl(change)}`;
}
_toggleCommitCollapsed() {
diff --git a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_test.html b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_test.html
index 4dc0e9b3..9f7dd69 100644
--- a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_test.html
+++ b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_test.html
@@ -1204,7 +1204,7 @@
.returns('/change/123');
assert.equal(
element._computeCopyTextForTitle(change),
- '123: test subject | https://localhost:8081/change/123'
+ `123: test subject | http://${location.host}/change/123`
);
});
diff --git a/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list.js b/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list.js
index a74ec5f..57337fb 100644
--- a/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list.js
+++ b/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list.js
@@ -167,7 +167,7 @@
return NaN;
}
- _computeReviewerTooltip(reviewer, change) {
+ _computeVoteableText(reviewer, change) {
if (!change || !change.labels) { return ''; }
const maxScores = [];
const maxPermitted = this._getMaxPermittedScores(change);
@@ -181,11 +181,7 @@
maxScores.push(`${label}`);
}
}
- if (maxScores.length) {
- return 'Votable: ' + maxScores.join(', ');
- } else {
- return '';
- }
+ return maxScores.join(', ');
}
_reviewersChanged(changeRecord, owner) {
diff --git a/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list_html.js b/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list_html.js
index bf7db12..c5df61d 100644
--- a/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list_html.js
+++ b/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list_html.js
@@ -44,7 +44,7 @@
</style>
<div class="container">
<template is="dom-repeat" items="[[_displayedReviewers]]" as="reviewer">
- <gr-account-chip class="reviewer" account="[[reviewer]]" on-remove="_handleRemove" additional-text="[[_computeReviewerTooltip(reviewer, change)]]" removable="[[_computeCanRemoveReviewer(reviewer, mutable)]]">
+ <gr-account-chip class="reviewer" account="[[reviewer]]" on-remove="_handleRemove" voteable-text="[[_computeVoteableText(reviewer, change)]]" removable="[[_computeCanRemoveReviewer(reviewer, mutable)]]">
</gr-account-chip>
</template>
<gr-button class="hiddenReviewers" link="" hidden\$="[[!_hiddenReviewerCount]]" on-click="_handleViewAll">and [[_hiddenReviewerCount]] more</gr-button>
diff --git a/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list_test.html b/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list_test.html
index 627fa10..32e2e9b6 100644
--- a/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list_test.html
+++ b/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list_test.html
@@ -329,13 +329,13 @@
},
};
assert.strictEqual(
- element._computeReviewerTooltip({_account_id: 1}, change),
- 'Votable: Bar');
+ element._computeVoteableText({_account_id: 1}, change),
+ 'Bar');
assert.strictEqual(
- element._computeReviewerTooltip({_account_id: 7}, change),
- 'Votable: Foo: +2, Bar, FooBar');
+ element._computeVoteableText({_account_id: 7}, change),
+ 'Foo: +2, Bar, FooBar');
assert.strictEqual(
- element._computeReviewerTooltip({_account_id: 2}, change),
+ element._computeVoteableText({_account_id: 2}, change),
'');
});
@@ -347,7 +347,7 @@
},
};
assert.strictEqual(
- element._computeReviewerTooltip({_account_id: 1}, change), '');
+ element._computeVoteableText({_account_id: 1}, change), '');
});
});
</script>
diff --git a/polygerrit-ui/app/elements/change/gr-upload-help-dialog/gr-upload-help-dialog_html.js b/polygerrit-ui/app/elements/change/gr-upload-help-dialog/gr-upload-help-dialog_html.js
index ccf22e6..3f4fc42 100644
--- a/polygerrit-ui/app/elements/change/gr-upload-help-dialog/gr-upload-help-dialog_html.js
+++ b/polygerrit-ui/app/elements/change/gr-upload-help-dialog/gr-upload-help-dialog_html.js
@@ -26,7 +26,7 @@
width: 100%;
}
ol {
- margin-left: var(--spacing-l);
+ margin-left: var(--spacing-xl);
list-style: decimal;
}
p {
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
index 6d9f9d7..444986b 100644
--- 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
@@ -124,7 +124,7 @@
}
_accountName(account) {
- return this.getUserName(this.config, account, true);
+ return this.getUserName(this.config, account);
}
}
diff --git a/polygerrit-ui/app/elements/core/gr-smart-search/gr-smart-search.js b/polygerrit-ui/app/elements/core/gr-smart-search/gr-smart-search.js
index a93c139..b27adf7 100644
--- a/polygerrit-ui/app/elements/core/gr-smart-search/gr-smart-search.js
+++ b/polygerrit-ui/app/elements/core/gr-smart-search/gr-smart-search.js
@@ -161,7 +161,7 @@
_mapAccountsHelper(accounts, predicate) {
return accounts.map(account => {
- const userName = this.getUserName(this._serverConfig, account, false);
+ const userName = this.getUserName(this._serverConfig, account);
return {
label: account.name || '',
text: account.email ?
diff --git a/polygerrit-ui/app/elements/gr-app-element.js b/polygerrit-ui/app/elements/gr-app-element.js
index 785e8f9..70806e2 100644
--- a/polygerrit-ui/app/elements/gr-app-element.js
+++ b/polygerrit-ui/app/elements/gr-app-element.js
@@ -497,6 +497,12 @@
}
_showKeyboardShortcuts(e) {
+ // same shortcut should close the dialog if pressed again
+ // when dialog is open
+ if (this.$.keyboardShortcuts.opened) {
+ this.$.keyboardShortcuts.close();
+ return;
+ }
if (this.shouldSuppressKeyboardShortcut(e)) { return; }
this.$.keyboardShortcuts.open();
}
diff --git a/polygerrit-ui/app/elements/settings/gr-account-info/gr-account-info.js b/polygerrit-ui/app/elements/settings/gr-account-info/gr-account-info.js
index c425318..0c4b706 100644
--- a/polygerrit-ui/app/elements/settings/gr-account-info/gr-account-info.js
+++ b/polygerrit-ui/app/elements/settings/gr-account-info/gr-account-info.js
@@ -63,11 +63,12 @@
type: Boolean,
notify: true,
computed: '_computeHasUnsavedChanges(_hasNameChange, ' +
- '_hasUsernameChange, _hasStatusChange)',
+ '_hasUsernameChange, _hasStatusChange, _hasDisplayNameChange)',
},
_hasNameChange: Boolean,
_hasUsernameChange: Boolean,
+ _hasDisplayNameChange: Boolean,
_hasStatusChange: Boolean,
_loading: {
type: Boolean,
@@ -95,6 +96,7 @@
return [
'_nameChanged(_account.name)',
'_statusChanged(_account.status)',
+ '_displayNameChanged(_account.display_name)',
];
}
@@ -110,6 +112,7 @@
promises.push(this.$.restAPI.getAccount().then(account => {
this._hasNameChange = false;
this._hasUsernameChange = false;
+ this._hasDisplayNameChange = false;
this._hasStatusChange = false;
// Provide predefined value for username to trigger computation of
// username mutability.
@@ -136,10 +139,12 @@
// Set only the fields that have changed.
// Must be done in sequence to avoid race conditions (@see Issue 5721)
return this._maybeSetName()
- .then(this._maybeSetUsername.bind(this))
- .then(this._maybeSetStatus.bind(this))
+ .then(() => this._maybeSetUsername())
+ .then(() => this._maybeSetDisplayName())
+ .then(() => this._maybeSetStatus())
.then(() => {
this._hasNameChange = false;
+ this._hasDisplayNameChange = false;
this._hasStatusChange = false;
this._saving = false;
this.fire('account-detail-update');
@@ -158,14 +163,22 @@
Promise.resolve();
}
+ _maybeSetDisplayName() {
+ return this._hasDisplayNameChange ?
+ this.$.restAPI.setAccountDisplayName(this._account.display_name) :
+ Promise.resolve();
+ }
+
_maybeSetStatus() {
return this._hasStatusChange ?
this.$.restAPI.setAccountStatus(this._account.status) :
Promise.resolve();
}
- _computeHasUnsavedChanges(nameChanged, usernameChanged, statusChanged) {
- return nameChanged || usernameChanged || statusChanged;
+ _computeHasUnsavedChanges(nameChanged, usernameChanged, statusChanged,
+ displayNameChanged) {
+ return nameChanged || usernameChanged || statusChanged
+ || displayNameChanged;
}
_computeUsernameMutable(config, username) {
@@ -191,6 +204,11 @@
this._hasStatusChange = true;
}
+ _displayNameChanged() {
+ if (this._loading) { return; }
+ this._hasDisplayNameChange = true;
+ }
+
_usernameChanged() {
if (this._loading || !this._account) { return; }
this._hasUsernameChange =
diff --git a/polygerrit-ui/app/elements/settings/gr-account-info/gr-account-info_html.js b/polygerrit-ui/app/elements/settings/gr-account-info/gr-account-info_html.js
index 6e37c25..0f5ddc8 100644
--- a/polygerrit-ui/app/elements/settings/gr-account-info/gr-account-info_html.js
+++ b/polygerrit-ui/app/elements/settings/gr-account-info/gr-account-info_html.js
@@ -79,6 +79,14 @@
</span>
</section>
<section>
+ <span class="title">Display name (defaults to "Full name")</span>
+ <span class="value">
+ <iron-input on-keydown="_handleKeydown" bind-value="{{_account.display_name}}">
+ <input is="iron-input" id="displayNameInput" disabled="[[_saving]]" on-keydown="_handleKeydown" bind-value="{{_account.display_name}}">
+ </iron-input>
+ </span>
+ </section>
+ <section>
<span class="title">Status (e.g. "Vacation")</span>
<span class="value">
<iron-input on-keydown="_handleKeydown" bind-value="{{_account.status}}">
diff --git a/polygerrit-ui/app/elements/shared/gr-account-chip/gr-account-chip.js b/polygerrit-ui/app/elements/shared/gr-account-chip/gr-account-chip.js
index 4ac540d..22fd1c20 100644
--- a/polygerrit-ui/app/elements/shared/gr-account-chip/gr-account-chip.js
+++ b/polygerrit-ui/app/elements/shared/gr-account-chip/gr-account-chip.js
@@ -56,7 +56,7 @@
static get properties() {
return {
account: Object,
- additionalText: String,
+ voteableText: String,
disabled: {
type: Boolean,
value: false,
diff --git a/polygerrit-ui/app/elements/shared/gr-account-chip/gr-account-chip_html.js b/polygerrit-ui/app/elements/shared/gr-account-chip/gr-account-chip_html.js
index 7f219e5..14bbd57 100644
--- a/polygerrit-ui/app/elements/shared/gr-account-chip/gr-account-chip_html.js
+++ b/polygerrit-ui/app/elements/shared/gr-account-chip/gr-account-chip_html.js
@@ -81,7 +81,7 @@
}
</style>
<div class\$="container [[_getBackgroundClass(transparentBackground)]]">
- <gr-account-link account="[[account]]" additional-text="[[additionalText]]">
+ <gr-account-link account="[[account]]" voteable-text="[[voteableText]]">
</gr-account-link>
<gr-button id="remove" link="" hidden\$="[[!removable]]" hidden="" tabindex="-1" aria-label="Remove" class\$="remove [[_getBackgroundClass(transparentBackground)]]" on-click="_handleRemoveTap">
<iron-icon icon="gr-icons:close"></iron-icon>
diff --git a/polygerrit-ui/app/elements/shared/gr-account-label/gr-account-label.js b/polygerrit-ui/app/elements/shared/gr-account-label/gr-account-label.js
index ba65e03..d279563 100644
--- a/polygerrit-ui/app/elements/shared/gr-account-label/gr-account-label.js
+++ b/polygerrit-ui/app/elements/shared/gr-account-label/gr-account-label.js
@@ -14,13 +14,14 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-import '../../../behaviors/gr-display-name-behavior/gr-display-name-behavior.js';
-
-import '../../../behaviors/gr-tooltip-behavior/gr-tooltip-behavior.js';
import '../../../scripts/bundled-polymer.js';
+
+import '@polymer/iron-icon/iron-icon.js';
+import '../../../behaviors/gr-display-name-behavior/gr-display-name-behavior.js';
+import '../../../behaviors/gr-tooltip-behavior/gr-tooltip-behavior.js';
import '../../../styles/shared-styles.js';
import '../gr-avatar/gr-avatar.js';
-import '../gr-limited-text/gr-limited-text.js';
+import '../gr-hovercard-account/gr-hovercard-account.js';
import '../gr-rest-api-interface/gr-rest-api-interface.js';
import '../../../scripts/util.js';
import {mixinBehaviors} from '@polymer/polymer/lib/legacy/class.js';
@@ -31,12 +32,10 @@
/**
* @appliesMixin Gerrit.DisplayNameMixin
- * @appliesMixin Gerrit.TooltipMixin
* @extends Polymer.Element
*/
class GrAccountLabel extends mixinBehaviors( [
Gerrit.DisplayNameBehavior,
- Gerrit.TooltipBehavior,
], GestureEventListeners(
LegacyElementMixin(
PolymerElement))) {
@@ -46,25 +45,11 @@
static get properties() {
return {
- /**
- * @type {{ name: string, status: string }}
- */
+ /**
+ * @type {{ name: string, status: string }}
+ */
account: Object,
- avatarImageSize: {
- type: Number,
- value: 32,
- },
- title: {
- type: String,
- reflectToAttribute: true,
- computed: '_computeAccountTitle(account, additionalText)',
- },
- additionalText: String,
- hasTooltip: {
- type: Boolean,
- reflectToAttribute: true,
- computed: '_computeHasTooltip(account)',
- },
+ voteableText: String,
hideAvatar: {
type: Boolean,
value: false,
@@ -79,68 +64,12 @@
/** @override */
ready() {
super.ready();
- if (!this.additionalText) { this.additionalText = ''; }
this.$.restAPI.getConfig()
.then(config => { this._serverConfig = config; });
}
_computeName(account, config) {
- return this.getUserName(config, account, false);
- }
-
- _computeStatusTextLength(account, config) {
- // 35 as the max length of the name + status
- return Math.max(10, 35 - this._computeName(account, config).length);
- }
-
- _computeAccountTitle(account, tooltip) {
- // Polymer 2: check for undefined
- if ([
- account,
- tooltip,
- ].some(arg => arg === undefined)) {
- return undefined;
- }
-
- if (!account) { return; }
- let result = '';
- if (this._computeName(account, this._serverConfig)) {
- result += this._computeName(account, this._serverConfig);
- }
- if (account.email) {
- result += ` <${account.email}>`;
- }
- if (this.additionalText) {
- result += ` ${this.additionalText}`;
- }
-
- // Show status in the label tooltip instead of
- // in a separate tooltip on status
- if (account.status) {
- result += ` (${account.status})`;
- }
-
- return result;
- }
-
- _computeShowEmailClass(account) {
- if (!account || account.name || !account.email) { return ''; }
- return 'showEmail';
- }
-
- _computeEmailStr(account) {
- if (!account || !account.email) {
- return '';
- }
- if (account.name) {
- return '(' + account.email + ')';
- }
- return account.email;
- }
-
- _computeHasTooltip(account) {
- // If an account has loaded to fire this method, then set to true.
- return !!account;
+ return this.getUserName(config, account);
}
}
diff --git a/polygerrit-ui/app/elements/shared/gr-account-label/gr-account-label_html.js b/polygerrit-ui/app/elements/shared/gr-account-label/gr-account-label_html.js
index e9d0e5d..a7d01ae 100644
--- a/polygerrit-ui/app/elements/shared/gr-account-label/gr-account-label_html.js
+++ b/polygerrit-ui/app/elements/shared/gr-account-label/gr-account-label_html.js
@@ -35,27 +35,24 @@
.text:hover {
@apply --gr-account-label-text-hover-style;
}
- .email,
- .showEmail .name {
- display: none;
- }
- .showEmail .email {
- display: inline-block;
+ iron-icon {
+ width: 14px;
+ height: 14px;
+ vertical-align: top;
+ position: relative;
+ top: 2px;
}
</style>
<span>
+ <gr-hovercard-account account="[[account]]" voteable-text="[[voteableText]]"></gr-hovercard-account>
<template is="dom-if" if="[[!hideAvatar]]">
- <gr-avatar account="[[account]]" image-size="[[avatarImageSize]]"></gr-avatar>
+ <gr-avatar account="[[account]]" image-size="32"></gr-avatar>
</template>
- <span class\$="text [[_computeShowEmailClass(account)]]">
+ <span class="text">
<span class="name">
[[_computeName(account, _serverConfig)]]</span>
- <span class="email">
- [[_computeEmailStr(account)]]
- </span>
<template is="dom-if" if="[[account.status]]">
- (<gr-limited-text disable-tooltip="true" limit="[[_computeStatusTextLength(account, _serverConfig)]]" text="[[account.status]]">
- </gr-limited-text>)
+ <iron-icon icon="gr-icons:calendar"></iron-icon>
</template>
</span>
</span>
diff --git a/polygerrit-ui/app/elements/shared/gr-account-label/gr-account-label_test.html b/polygerrit-ui/app/elements/shared/gr-account-label/gr-account-label_test.html
index db742e6..fd16350 100644
--- a/polygerrit-ui/app/elements/shared/gr-account-label/gr-account-label_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-account-label/gr-account-label_test.html
@@ -70,54 +70,6 @@
});
});
- test('missing email', () => {
- assert.equal('', element._computeEmailStr({name: 'foo'}));
- });
-
- test('computed fields', () => {
- assert.equal(
- element._computeAccountTitle({
- name: 'Andrew Bonventre',
- email: 'andybons+gerrit@gmail.com',
- }, /* additionalText= */ ''),
- 'Andrew Bonventre <andybons+gerrit@gmail.com>');
-
- assert.equal(
- element._computeAccountTitle({
- name: 'Andrew Bonventre',
- }, /* additionalText= */ ''),
- 'Andrew Bonventre');
-
- assert.equal(
- element._computeAccountTitle({
- email: 'andybons+gerrit@gmail.com',
- }, /* additionalText= */ ''),
- 'Anonymous <andybons+gerrit@gmail.com>');
-
- assert.equal(element._computeShowEmailClass(
- {
- name: 'Andrew Bonventre',
- email: 'andybons+gerrit@gmail.com',
- }, /* additionalText= */ ''), '');
-
- assert.equal(element._computeShowEmailClass(
- {
- email: 'andybons+gerrit@gmail.com',
- }, /* additionalText= */ ''), 'showEmail');
-
- assert.equal(element._computeShowEmailClass(
- {name: 'Andrew Bonventre'},
- /* additionalText= */ ''
- ),
- '');
-
- assert.equal(element._computeShowEmailClass(undefined), '');
-
- assert.equal(
- element._computeEmailStr({name: 'test', email: 'test'}), '(test)');
- assert.equal(element._computeEmailStr({email: 'test'}, ''), 'test');
- });
-
suite('_computeName', () => {
test('not showing anonymous', () => {
const account = {name: 'Wyatt'};
@@ -152,45 +104,5 @@
'TestAnon');
});
});
-
- suite('status in tooltip', () => {
- setup(() => {
- element = fixture('basic');
- element.account = {
- name: 'test',
- email: 'test@google.com',
- status: 'OOO until Aug 10th',
- };
- element._config = {
- user: {
- anonymous_coward_name: 'Anonymous Coward',
- },
- };
- });
-
- test('tooltip should contain status text', () => {
- assert.deepEqual(element.title,
- 'test <test@google.com> (OOO until Aug 10th)');
- });
-
- test('status text should not have tooltip', () => {
- flushAsynchronousOperations();
- assert.deepEqual(element.shadowRoot
- .querySelector('gr-limited-text').title, '');
- });
-
- test('status text should honor the name length and total length', () => {
- assert.deepEqual(
- element._computeStatusTextLength(element.account, element._config),
- 31
- );
- assert.deepEqual(
- element._computeStatusTextLength({
- name: 'a very long long long long name',
- }, element._config),
- 10
- );
- });
- });
});
</script>
diff --git a/polygerrit-ui/app/elements/shared/gr-account-link/gr-account-link.js b/polygerrit-ui/app/elements/shared/gr-account-link/gr-account-link.js
index 4a38427..e0d5583 100644
--- a/polygerrit-ui/app/elements/shared/gr-account-link/gr-account-link.js
+++ b/polygerrit-ui/app/elements/shared/gr-account-link/gr-account-link.js
@@ -41,12 +41,8 @@
static get properties() {
return {
- additionalText: String,
+ voteableText: String,
account: Object,
- avatarImageSize: {
- type: Number,
- value: 32,
- },
};
}
diff --git a/polygerrit-ui/app/elements/shared/gr-account-link/gr-account-link_html.js b/polygerrit-ui/app/elements/shared/gr-account-link/gr-account-link_html.js
index 4ea343e..4f1ea44 100644
--- a/polygerrit-ui/app/elements/shared/gr-account-link/gr-account-link_html.js
+++ b/polygerrit-ui/app/elements/shared/gr-account-link/gr-account-link_html.js
@@ -33,7 +33,7 @@
</style>
<span>
<a href\$="[[_computeOwnerLink(account)]]" tabindex="-1">
- <gr-account-label account="[[account]]" additional-text="[[additionalText]]" avatar-image-size="[[avatarImageSize]]"></gr-account-label>
+ <gr-account-label account="[[account]]" voteable-text="[[voteableText]]"></gr-account-label>
</a>
</span>
`;
diff --git a/polygerrit-ui/app/elements/shared/gr-button/gr-button.js b/polygerrit-ui/app/elements/shared/gr-button/gr-button.js
index cde56df..41d958fd 100644
--- a/polygerrit-ui/app/elements/shared/gr-button/gr-button.js
+++ b/polygerrit-ui/app/elements/shared/gr-button/gr-button.js
@@ -26,6 +26,7 @@
import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
import {PolymerElement} from '@polymer/polymer/polymer-element.js';
import {htmlTemplate} from './gr-button_html.js';
+import '../../../scripts/util.js';
/**
* @appliesMixin Gerrit.KeyboardShortcutMixin
@@ -105,19 +106,8 @@
return;
}
- let el = this.root;
- let path = '';
- while (el = el.parentNode || el.host) {
- if (el.tagName && el.tagName.startsWith('GR-APP')) {
- break;
- }
- if (el.tagName) {
- const idString = el.id ? '#' + el.id : '';
- path = el.tagName + idString + ' ' + path;
- }
- }
this.$.reporting.reportInteraction('button-click',
- {path: path.trim().toLowerCase()});
+ {path: util.getEventPath(e)});
}
_disabledChanged(disabled) {
diff --git a/polygerrit-ui/app/elements/shared/gr-button/gr-button_test.html b/polygerrit-ui/app/elements/shared/gr-button/gr-button_test.html
index 42f9a5a..f2ce4ba 100644
--- a/polygerrit-ui/app/elements/shared/gr-button/gr-button_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-button/gr-button_test.html
@@ -40,6 +40,14 @@
</template>
</test-fixture>
+<test-fixture id="nested">
+ <template>
+ <div id="test">
+ <gr-button class="testBtn"></gr-button>
+ </div>
+ </template>
+</test-fixture>
+
<test-fixture id="tabindex">
<template>
<gr-button tabindex="3"></gr-button>
@@ -190,5 +198,36 @@
});
}
});
+
+ suite('reporting', () => {
+ const reportStub = sinon.stub();
+ setup(() => {
+ stub('gr-reporting', {
+ reportInteraction: (...args) => {
+ reportStub(...args);
+ },
+ });
+ reportStub.reset();
+ });
+
+ test('report event after click', () => {
+ MockInteractions.click(element);
+ assert.isTrue(reportStub.calledOnce);
+ assert.equal(reportStub.lastCall.args[0], 'button-click');
+ assert.deepEqual(reportStub.lastCall.args[1], {
+ path: 'html>body>test-fixture#basic>gr-button',
+ });
+ });
+
+ test('report event after click on nested', () => {
+ element = fixture('nested');
+ MockInteractions.click(element.querySelector('gr-button'));
+ assert.isTrue(reportStub.calledOnce);
+ assert.equal(reportStub.lastCall.args[0], 'button-click');
+ assert.deepEqual(reportStub.lastCall.args[1], {
+ path: 'html>body>test-fixture#nested>div#test>gr-button.testBtn',
+ });
+ });
+ });
});
</script>
diff --git a/polygerrit-ui/app/elements/shared/gr-formatted-text/gr-formatted-text_html.js b/polygerrit-ui/app/elements/shared/gr-formatted-text/gr-formatted-text_html.js
index a30b65a..2b50565 100644
--- a/polygerrit-ui/app/elements/shared/gr-formatted-text/gr-formatted-text_html.js
+++ b/polygerrit-ui/app/elements/shared/gr-formatted-text/gr-formatted-text_html.js
@@ -48,7 +48,7 @@
}
code {
display: block;
- white-space: pre;
+ white-space: pre-wrap;
color: var(--deemphasized-text-color);
}
li {
diff --git a/polygerrit-ui/app/elements/shared/gr-hovercard-account/gr-hovercard-account.js b/polygerrit-ui/app/elements/shared/gr-hovercard-account/gr-hovercard-account.js
new file mode 100644
index 0000000..0bc9cb7
--- /dev/null
+++ b/polygerrit-ui/app/elements/shared/gr-hovercard-account/gr-hovercard-account.js
@@ -0,0 +1,50 @@
+/**
+ * @license
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import '../../../scripts/bundled-polymer.js';
+
+import '@polymer/iron-icon/iron-icon.js';
+import '../../../styles/shared-styles.js';
+import '../gr-avatar/gr-avatar.js';
+import '../gr-button/gr-button.js';
+import {hovercardBehaviorMixin} from '../gr-hovercard/gr-hovercard-behavior.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-hovercard-account_html.js';
+
+/** @extends Polymer.Element */
+class GrHovercardAccount extends GestureEventListeners(
+ hovercardBehaviorMixin(LegacyElementMixin(
+ PolymerElement))) {
+ static get template() { return htmlTemplate; }
+
+ static get is() { return 'gr-hovercard-account'; }
+
+ static get properties() {
+ return {
+ account: Object,
+ voteableText: String,
+ attention: {
+ type: Boolean,
+ value: false,
+ reflectToAttribute: true,
+ },
+ };
+ }
+}
+
+customElements.define(GrHovercardAccount.is, GrHovercardAccount);
diff --git a/polygerrit-ui/app/elements/shared/gr-hovercard-account/gr-hovercard-account_html.js b/polygerrit-ui/app/elements/shared/gr-hovercard-account/gr-hovercard-account_html.js
new file mode 100644
index 0000000..0763420
--- /dev/null
+++ b/polygerrit-ui/app/elements/shared/gr-hovercard-account/gr-hovercard-account_html.js
@@ -0,0 +1,96 @@
+/**
+ * @license
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import '../gr-hovercard/gr-hovercard-shared-style.js';
+import {html} from '@polymer/polymer/lib/utils/html-tag.js';
+
+export const htmlTemplate = html`
+ <style include="gr-hovercard-shared-style">
+ .top,
+ .attention,
+ .status,
+ .voteable {
+ padding: var(--spacing-s) var(--spacing-l);
+ }
+ .top {
+ display: flex;
+ padding-top: var(--spacing-xl);
+ min-width: 300px;
+ }
+ gr-avatar {
+ height: 48px;
+ width: 48px;
+ margin-right: var(--spacing-l);
+ }
+ .title,
+ .email {
+ color: var(--deemphasized-text-color);
+ }
+ .status iron-icon {
+ width: 14px;
+ height: 14px;
+ vertical-align: top;
+ position: relative;
+ top: 2px;
+ }
+ .action {
+ border-top: 1px solid var(--border-color);
+ padding: var(--spacing-s) var(--spacing-l);
+ --gr-button: {
+ padding: var(--spacing-s) 0;
+ };
+ }
+ :host(:not([attention])) .attention {
+ display: none;
+ }
+ .attention {
+ background-color: var(--emphasis-color);
+ }
+ .attention iron-icon {
+ vertical-align: top;
+ }
+ </style>
+ <div id="container" role="tooltip" tabindex="-1">
+ <div class="top">
+ <div class="avatar">
+ <gr-avatar account="[[account]]" image-size="56"></gr-avatar>
+ </div>
+ <div class="account">
+ <h3 class="name">[[account.name]]</h3>
+ <div class="email">[[account.email]]</div>
+ </div>
+ </div>
+ <template is="dom-if" if="[[account.status]]">
+ <div class="status">
+ <span class="title">
+ <iron-icon icon="gr-icons:calendar"></iron-icon>
+ Status:
+ </span>
+ <span class="value">[[account.status]]</span>
+ </div>
+ </template>
+ <template is="dom-if" if="[[voteableText]]">
+ <div class="voteable">
+ <span class="title">Voteable:</span>
+ <span class="value">[[voteableText]]</span>
+ </div>
+ </template>
+ <div class="attention">
+ <iron-icon icon="gr-icons:attention"></iron-icon>
+ <span>It is this user's turn to take action.</span>
+ </div>
+ </div>
+`;
diff --git a/polygerrit-ui/app/elements/shared/gr-hovercard-account/gr-hovercard-account_test.html b/polygerrit-ui/app/elements/shared/gr-hovercard-account/gr-hovercard-account_test.html
new file mode 100644
index 0000000..7a5f4c6
--- /dev/null
+++ b/polygerrit-ui/app/elements/shared/gr-hovercard-account/gr-hovercard-account_test.html
@@ -0,0 +1,94 @@
+<!DOCTYPE html>
+<!--
+@license
+Copyright (C) 2020 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+
+<meta name="viewport"
+ content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
+<title>gr-hovercard-account</title>
+
+<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
+
+<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/components/wct-browser-legacy/browser.js"></script>
+<script type="module" src="../../../test/test-pre-setup.js"></script>
+<script type="module" src="../../../test/common-test-setup.js"></script>
+<script src="../../../node_modules/iron-test-helpers/mock-interactions.js" type="module"></script>
+
+<script type="module" src="./gr-hovercard-account.js"></script>
+
+<script type="module">
+ import '../../../test/test-pre-setup.js';
+ import '../../../test/common-test-setup.js';
+ import './gr-hovercard-account.js';
+
+ void (0);
+</script>
+
+<test-fixture id="basic">
+ <template>
+ <gr-hovercard-account class="hovered"></gr-hovercard-account>
+ </template>
+</test-fixture>
+
+
+<script type="module">
+ import '../../../test/test-pre-setup.js';
+ import '../../../test/common-test-setup.js';
+ import './gr-hovercard-account.js';
+
+ suite('gr-hovercard-account tests', () => {
+ let element;
+ const ACCOUNT = {
+ email: 'kermit@gmail.com',
+ username: 'kermit',
+ name: 'Kermit The Frog',
+ _account_id: '31415926535',
+ };
+
+ setup(() => {
+ element = fixture('basic');
+ element.account = Object.assign({}, ACCOUNT);
+ });
+
+ test('account name is shown', () => {
+ assert.equal(element.shadowRoot.querySelector('.name').innerText,
+ 'Kermit The Frog');
+ });
+
+ test('account status is not shown if the property is not set', () => {
+ assert.isNull(element.shadowRoot.querySelector('.status'));
+ });
+
+ test('account status is displayed', () => {
+ element.account = Object.assign({status: 'OOO'}, ACCOUNT);
+ flushAsynchronousOperations();
+ assert.equal(element.shadowRoot.querySelector('.status .value').innerText,
+ 'OOO');
+ });
+
+ test('voteable div is not shown if the property is not set', () => {
+ assert.isNull(element.shadowRoot.querySelector('.voteable'));
+ });
+
+ test('voteable div is displayed', () => {
+ element.voteableText = 'CodeReview: +2';
+ flushAsynchronousOperations();
+ assert.equal(element.shadowRoot.querySelector('.voteable .value').innerText,
+ element.voteableText);
+ });
+ });
+</script>
diff --git a/polygerrit-ui/app/elements/shared/gr-hovercard/gr-hovercard-behavior.js b/polygerrit-ui/app/elements/shared/gr-hovercard/gr-hovercard-behavior.js
new file mode 100644
index 0000000..a77f5f7
--- /dev/null
+++ b/polygerrit-ui/app/elements/shared/gr-hovercard/gr-hovercard-behavior.js
@@ -0,0 +1,354 @@
+/**
+ * @license
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import '../../../scripts/bundled-polymer.js';
+
+import '../../../styles/shared-styles.js';
+import '../../../scripts/rootElement.js';
+import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js';
+
+const HOVER_CLASS = 'hovered';
+
+/**
+ * When the hovercard is positioned diagonally (bottom-left, bottom-right,
+ * top-left, or top-right), we add additional (invisible) padding so that the
+ * area that a user can hover over to access the hovercard is larger.
+ */
+const DIAGONAL_OVERFLOW = 15;
+
+/**
+ * The mixin for gr-hovercard-behavior.
+ *
+ * @example
+ *
+ * // LegacyElementMixin is still needed to support the old lifecycles
+ * // TODO: Replace old life cycles with new ones.
+ *
+ * class YourComponent extends hovercardBehaviorMixin(
+ * LegacyElementMixin(PolymerElement)
+ * ) {
+ * static get is() { return ''; }
+ * static get template() { return html``; }
+ * }
+ *
+ * customElements.define(GrHovercard.is, GrHovercard);
+ *
+ * @see gr-hovercard.js
+ *
+ * // following annotations are required for polylint
+ * @polymer
+ * @mixinFunction
+ */
+export const hovercardBehaviorMixin = superClass => class extends superClass {
+ static get properties() {
+ return {
+ /**
+ * @type {?}
+ */
+ _target: Object,
+
+ /**
+ * Determines whether or not the hovercard is visible.
+ *
+ * @type {boolean}
+ */
+ _isShowing: {
+ type: Boolean,
+ value: false,
+ },
+ /**
+ * The `id` of the element that the hovercard is anchored to.
+ *
+ * @type {string}
+ */
+ for: {
+ type: String,
+ observer: '_forChanged',
+ },
+
+ /**
+ * The spacing between the top of the hovercard and the element it is
+ * anchored to.
+ *
+ * @type {number}
+ */
+ offset: {
+ type: Number,
+ value: 14,
+ },
+
+ /**
+ * Positions the hovercard to the top, right, bottom, left, bottom-left,
+ * bottom-right, top-left, or top-right of its content.
+ *
+ * @type {string}
+ */
+ position: {
+ type: String,
+ value: 'right',
+ },
+
+ container: Object,
+ /**
+ * ID for the container element.
+ *
+ * @type {string}
+ */
+ containerId: {
+ type: String,
+ value: 'gr-hovercard-container',
+ },
+ };
+ }
+
+ /** @override */
+ attached() {
+ super.attached();
+ if (!this._target) { this._target = this.target; }
+ this.listen(this._target, 'mouseenter', 'show');
+ this.listen(this._target, 'focus', 'show');
+ this.listen(this._target, 'mouseleave', 'hide');
+ this.listen(this._target, 'blur', 'hide');
+ this.listen(this._target, 'click', 'hide');
+ }
+
+ /** @override */
+ created() {
+ super.created();
+ this.addEventListener('mouseleave',
+ e => this.hide(e));
+ }
+
+ /** @override */
+ ready() {
+ super.ready();
+ // First, check to see if the container has already been created.
+ this.container = Gerrit.getRootElement()
+ .querySelector('#' + this.containerId);
+
+ if (this.container) { return; }
+
+ // If it does not exist, create and initialize the hovercard container.
+ this.container = document.createElement('div');
+ this.container.setAttribute('id', this.containerId);
+ Gerrit.getRootElement().appendChild(this.container);
+ }
+
+ removeListeners() {
+ this.unlisten(this._target, 'mouseenter', 'show');
+ this.unlisten(this._target, 'focus', 'show');
+ this.unlisten(this._target, 'mouseleave', 'hide');
+ this.unlisten(this._target, 'blur', 'hide');
+ this.unlisten(this._target, 'click', 'hide');
+ }
+
+ /**
+ * Returns the target element that the hovercard is anchored to (the `id` of
+ * the `for` property).
+ *
+ * @type {HTMLElement}
+ */
+ get target() {
+ const parentNode = dom(this).parentNode;
+ // If the parentNode is a document fragment, then we need to use the host.
+ const ownerRoot = dom(this).getOwnerRoot();
+ let target;
+ if (this.for) {
+ target = dom(ownerRoot).querySelector('#' + this.for);
+ } else {
+ target = parentNode.nodeType == Node.DOCUMENT_FRAGMENT_NODE ?
+ ownerRoot.host :
+ parentNode;
+ }
+ return target;
+ }
+
+ /**
+ * Hides/closes the hovercard. This occurs when the user triggers the
+ * `mouseleave` event on the hovercard's `target` element (as long as the
+ * user is not hovering over the hovercard).
+ *
+ * @param {Event} e DOM Event (e.g. `mouseleave` event)
+ */
+ hide(e) {
+ const targetRect = this._target.getBoundingClientRect();
+ const x = e.clientX;
+ const y = e.clientY;
+ if (x > targetRect.left && x < targetRect.right && y > targetRect.top &&
+ y < targetRect.bottom) {
+ // Sometimes the hovercard itself obscures the mouse pointer, and
+ // that generates a mouseleave event. We don't want to hide the hovercard
+ // in that situation.
+ return;
+ }
+
+ // If the hovercard is already hidden or the user is now hovering over the
+ // hovercard or the user is returning from the hovercard but now hovering
+ // over the target (to stop an annoying flicker effect), just return.
+ if (!this._isShowing || e.toElement === this ||
+ (e.fromElement === this && e.toElement === this._target)) {
+ return;
+ }
+
+ // Mark that the hovercard is not visible and do not allow focusing
+ this._isShowing = false;
+
+ // Clear styles in preparation for the next time we need to show the card
+ this.classList.remove(HOVER_CLASS);
+
+ // Reset and remove the hovercard from the DOM
+ this.style.cssText = '';
+ this.$.container.setAttribute('tabindex', -1);
+
+ // Remove the hovercard from the container, given that it is still a child
+ // of the container.
+ if (this.container.contains(this)) {
+ this.container.removeChild(this);
+ }
+ }
+
+ /**
+ * Shows/opens the hovercard. This occurs when the user triggers the
+ * `mousenter` event on the hovercard's `target` element.
+ *
+ * @param {Event} e DOM Event (e.g., `mouseenter` event)
+ */
+ show(e) {
+ if (this._isShowing) {
+ return;
+ }
+
+ // Mark that the hovercard is now visible
+ this._isShowing = true;
+ this.setAttribute('tabindex', 0);
+
+ // Add it to the DOM and calculate its position
+ this.container.appendChild(this);
+ this.updatePosition();
+
+ // Trigger the transition
+ this.classList.add(HOVER_CLASS);
+ }
+
+ /**
+ * Updates the hovercard's position based on the `position` attribute
+ * and the current position of the `target` element.
+ *
+ * The hovercard is supposed to stay open if the user hovers over it.
+ * To keep it open when the user moves away from the target, the bounding
+ * rects of the target and hovercard must touch or overlap.
+ *
+ * NOTE: You do not need to directly call this method unless you need to
+ * update the position of the tooltip while it is already visible (the
+ * target element has moved and the tooltip is still open).
+ */
+ updatePosition() {
+ if (!this._target) { return; }
+
+ // Calculate the necessary measurements and positions
+ const parentRect = document.documentElement.getBoundingClientRect();
+ const targetRect = this._target.getBoundingClientRect();
+ const thisRect = this.getBoundingClientRect();
+
+ const targetLeft = targetRect.left - parentRect.left;
+ const targetTop = targetRect.top - parentRect.top;
+
+ let hovercardLeft;
+ let hovercardTop;
+ const diagonalPadding = this.offset + DIAGONAL_OVERFLOW;
+ let cssText = '';
+
+ // Find the top and left position values based on the position attribute
+ // of the hovercard.
+ switch (this.position) {
+ case 'top':
+ hovercardLeft = targetLeft + (targetRect.width - thisRect.width) / 2;
+ hovercardTop = targetTop - thisRect.height - this.offset;
+ cssText += `padding-bottom:${this.offset
+ }px; margin-bottom:-${this.offset}px;`;
+ break;
+ case 'bottom':
+ hovercardLeft = targetLeft + (targetRect.width - thisRect.width) / 2;
+ hovercardTop = targetTop + targetRect.height + this.offset;
+ cssText +=
+ `padding-top:${this.offset}px; margin-top:-${this.offset}px;`;
+ break;
+ case 'left':
+ hovercardLeft = targetLeft - thisRect.width - this.offset;
+ hovercardTop = targetTop + (targetRect.height - thisRect.height) / 2;
+ cssText +=
+ `padding-right:${this.offset}px; margin-right:-${this.offset}px;`;
+ break;
+ case 'right':
+ hovercardLeft = targetRect.right + this.offset;
+ hovercardTop = targetTop + (targetRect.height - thisRect.height) / 2;
+ cssText +=
+ `padding-left:${this.offset}px; margin-left:-${this.offset}px;`;
+ break;
+ case 'bottom-right':
+ hovercardLeft = targetRect.left + targetRect.width + this.offset;
+ hovercardTop = targetRect.top + targetRect.height + this.offset;
+ cssText += `padding-top:${diagonalPadding}px;`;
+ cssText += `padding-left:${diagonalPadding}px;`;
+ cssText += `margin-left:-${diagonalPadding}px;`;
+ cssText += `margin-top:-${diagonalPadding}px;`;
+ break;
+ case 'bottom-left':
+ hovercardLeft = targetRect.left - thisRect.width - this.offset;
+ hovercardTop = targetRect.top + targetRect.height + this.offset;
+ cssText += `padding-top:${diagonalPadding}px;`;
+ cssText += `padding-right:${diagonalPadding}px;`;
+ cssText += `margin-right:-${diagonalPadding}px;`;
+ cssText += `margin-top:-${diagonalPadding}px;`;
+ break;
+ case 'top-left':
+ hovercardLeft = targetRect.left - thisRect.width - this.offset;
+ hovercardTop = targetRect.top - thisRect.height - this.offset;
+ cssText += `padding-bottom:${diagonalPadding}px;`;
+ cssText += `padding-right:${diagonalPadding}px;`;
+ cssText += `margin-bottom:-${diagonalPadding}px;`;
+ cssText += `margin-right:-${diagonalPadding}px;`;
+ break;
+ case 'top-right':
+ hovercardLeft = targetRect.left + targetRect.width + this.offset;
+ hovercardTop = targetRect.top - thisRect.height - this.offset;
+ cssText += `padding-bottom:${diagonalPadding}px;`;
+ cssText += `padding-left:${diagonalPadding}px;`;
+ cssText += `margin-bottom:-${diagonalPadding}px;`;
+ cssText += `margin-left:-${diagonalPadding}px;`;
+ break;
+ }
+
+ // Prevent hovercard from appearing outside the viewport.
+ // TODO(kaspern): fix hovercard appearing outside viewport on bottom and
+ // right.
+ if (hovercardLeft < 0) { hovercardLeft = 0; }
+ if (hovercardTop < 0) { hovercardTop = 0; }
+ // Set the hovercard's position
+ cssText += `left:${hovercardLeft}px; top:${hovercardTop}px;`;
+ this.style.cssText = cssText;
+ }
+
+ /**
+ * Responds to a change in the `for` value and gets the updated `target`
+ * element for the hovercard.
+ *
+ * @private
+ */
+ _forChanged() {
+ this._target = this.target;
+ }
+};
diff --git a/polygerrit-ui/app/elements/shared/gr-hovercard/gr-hovercard-shared-style.js b/polygerrit-ui/app/elements/shared/gr-hovercard/gr-hovercard-shared-style.js
new file mode 100644
index 0000000..bb81cfd
--- /dev/null
+++ b/polygerrit-ui/app/elements/shared/gr-hovercard/gr-hovercard-shared-style.js
@@ -0,0 +1,45 @@
+/**
+ * @license
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/** The shared styles for all hover cards. */
+const GrHoverCardSharedStyle = document.createElement('dom-module');
+GrHoverCardSharedStyle.innerHTML =
+ `<template>
+ <style include="shared-styles">
+ :host {
+ box-sizing: border-box;
+ opacity: 0;
+ position: absolute;
+ transition: opacity 200ms;
+ visibility: hidden;
+ z-index: 200;
+ }
+ :host(.hovered) {
+ visibility: visible;
+ opacity: 1;
+ }
+ /* You have to use a <div class="container"> in your hovercard in order
+ to pick up this consistent styling. */
+ #container {
+ background: var(--dialog-background-color);
+ border-radius: var(--border-radius);
+ box-shadow: var(--elevation-level-5);
+ }
+ </style>
+ </template>`;
+
+GrHoverCardSharedStyle.register('gr-hovercard-shared-style');
diff --git a/polygerrit-ui/app/elements/shared/gr-hovercard/gr-hovercard.js b/polygerrit-ui/app/elements/shared/gr-hovercard/gr-hovercard.js
index ce2303f..3f936dd 100644
--- a/polygerrit-ui/app/elements/shared/gr-hovercard/gr-hovercard.js
+++ b/polygerrit-ui/app/elements/shared/gr-hovercard/gr-hovercard.js
@@ -18,327 +18,20 @@
import '../../../styles/shared-styles.js';
import '../../../scripts/rootElement.js';
-import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.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-hovercard_html.js';
-
-const HOVER_CLASS = 'hovered';
-
-/**
- * When the hovercard is positioned diagonally (bottom-left, bottom-right,
- * top-left, or top-right), we add additional (invisible) padding so that the
- * area that a user can hover over to access the hovercard is larger.
- */
-const DIAGONAL_OVERFLOW = 15;
+import {hovercardBehaviorMixin} from './gr-hovercard-behavior.js';
+import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
+import './gr-hovercard-shared-style.js';
/** @extends Polymer.Element */
class GrHovercard extends GestureEventListeners(
- LegacyElementMixin(
- PolymerElement)) {
+ hovercardBehaviorMixin(LegacyElementMixin(PolymerElement))
+) {
static get template() { return htmlTemplate; }
static get is() { return 'gr-hovercard'; }
-
- static get properties() {
- return {
- /**
- * @type {?}
- */
- _target: Object,
-
- /**
- * Determines whether or not the hovercard is visible.
- *
- * @type {boolean}
- */
- _isShowing: {
- type: Boolean,
- value: false,
- },
- /**
- * The `id` of the element that the hovercard is anchored to.
- *
- * @type {string}
- */
- for: {
- type: String,
- observer: '_forChanged',
- },
-
- /**
- * The spacing between the top of the hovercard and the element it is
- * anchored to.
- *
- * @type {number}
- */
- offset: {
- type: Number,
- value: 14,
- },
-
- /**
- * Positions the hovercard to the top, right, bottom, left, bottom-left,
- * bottom-right, top-left, or top-right of its content.
- *
- * @type {string}
- */
- position: {
- type: String,
- value: 'bottom',
- },
-
- container: Object,
- /**
- * ID for the container element.
- *
- * @type {string}
- */
- containerId: {
- type: String,
- value: 'gr-hovercard-container',
- },
- };
- }
-
- /** @override */
- attached() {
- super.attached();
- if (!this._target) { this._target = this.target; }
- this.listen(this._target, 'mouseenter', 'show');
- this.listen(this._target, 'focus', 'show');
- this.listen(this._target, 'mouseleave', 'hide');
- this.listen(this._target, 'blur', 'hide');
- this.listen(this._target, 'click', 'hide');
- }
-
- /** @override */
- created() {
- super.created();
- this.addEventListener('mouseleave',
- e => this.hide(e));
- }
-
- /** @override */
- ready() {
- super.ready();
- // First, check to see if the container has already been created.
- this.container = Gerrit.getRootElement()
- .querySelector('#' + this.containerId);
-
- if (this.container) { return; }
-
- // If it does not exist, create and initialize the hovercard container.
- this.container = document.createElement('div');
- this.container.setAttribute('id', this.containerId);
- Gerrit.getRootElement().appendChild(this.container);
- }
-
- removeListeners() {
- this.unlisten(this._target, 'mouseenter', 'show');
- this.unlisten(this._target, 'focus', 'show');
- this.unlisten(this._target, 'mouseleave', 'hide');
- this.unlisten(this._target, 'blur', 'hide');
- this.unlisten(this._target, 'click', 'hide');
- }
-
- /**
- * Returns the target element that the hovercard is anchored to (the `id` of
- * the `for` property).
- *
- * @type {HTMLElement}
- */
- get target() {
- const parentNode = dom(this).parentNode;
- // If the parentNode is a document fragment, then we need to use the host.
- const ownerRoot = dom(this).getOwnerRoot();
- let target;
- if (this.for) {
- target = dom(ownerRoot).querySelector('#' + this.for);
- } else {
- target = parentNode.nodeType == Node.DOCUMENT_FRAGMENT_NODE ?
- ownerRoot.host :
- parentNode;
- }
- return target;
- }
-
- /**
- * Hides/closes the hovercard. This occurs when the user triggers the
- * `mouseleave` event on the hovercard's `target` element (as long as the
- * user is not hovering over the hovercard).
- *
- * @param {Event} e DOM Event (e.g. `mouseleave` event)
- */
- hide(e) {
- const targetRect = this._target.getBoundingClientRect();
- const x = e.clientX;
- const y = e.clientY;
- if (x > targetRect.left && x < targetRect.right && y > targetRect.top &&
- y < targetRect.bottom) {
- // Sometimes the hovercard itself obscures the mouse pointer, and
- // that generates a mouseleave event. We don't want to hide the hovercard
- // in that situation.
- return;
- }
-
- // If the hovercard is already hidden or the user is now hovering over the
- // hovercard or the user is returning from the hovercard but now hovering
- // over the target (to stop an annoying flicker effect), just return.
- if (!this._isShowing || e.toElement === this ||
- (e.fromElement === this && e.toElement === this._target)) {
- return;
- }
-
- // Mark that the hovercard is not visible and do not allow focusing
- this._isShowing = false;
-
- // Clear styles in preparation for the next time we need to show the card
- this.classList.remove(HOVER_CLASS);
-
- // Reset and remove the hovercard from the DOM
- this.style.cssText = '';
- this.$.hovercard.setAttribute('tabindex', -1);
-
- // Remove the hovercard from the container, given that it is still a child
- // of the container.
- if (this.container.contains(this)) {
- this.container.removeChild(this);
- }
- }
-
- /**
- * Shows/opens the hovercard. This occurs when the user triggers the
- * `mousenter` event on the hovercard's `target` element.
- *
- * @param {Event} e DOM Event (e.g., `mouseenter` event)
- */
- show(e) {
- if (this._isShowing) {
- return;
- }
-
- // Mark that the hovercard is now visible
- this._isShowing = true;
- this.setAttribute('tabindex', 0);
-
- // Add it to the DOM and calculate its position
- this.container.appendChild(this);
- this.updatePosition();
-
- // Trigger the transition
- this.classList.add(HOVER_CLASS);
- }
-
- /**
- * Updates the hovercard's position based on the `position` attribute
- * and the current position of the `target` element.
- *
- * The hovercard is supposed to stay open if the user hovers over it.
- * To keep it open when the user moves away from the target, the bounding
- * rects of the target and hovercard must touch or overlap.
- *
- * NOTE: You do not need to directly call this method unless you need to
- * update the position of the tooltip while it is already visible (the
- * target element has moved and the tooltip is still open).
- */
- updatePosition() {
- if (!this._target) { return; }
-
- // Calculate the necessary measurements and positions
- const parentRect = document.documentElement.getBoundingClientRect();
- const targetRect = this._target.getBoundingClientRect();
- const thisRect = this.getBoundingClientRect();
-
- const targetLeft = targetRect.left - parentRect.left;
- const targetTop = targetRect.top - parentRect.top;
-
- let hovercardLeft;
- let hovercardTop;
- const diagonalPadding = this.offset + DIAGONAL_OVERFLOW;
- let cssText = '';
-
- // Find the top and left position values based on the position attribute
- // of the hovercard.
- switch (this.position) {
- case 'top':
- hovercardLeft = targetLeft + (targetRect.width - thisRect.width) / 2;
- hovercardTop = targetTop - thisRect.height - this.offset;
- cssText += `padding-bottom:${this.offset
- }px; margin-bottom:-${this.offset}px;`;
- break;
- case 'bottom':
- hovercardLeft = targetLeft + (targetRect.width - thisRect.width) / 2;
- hovercardTop = targetTop + targetRect.height + this.offset;
- cssText +=
- `padding-top:${this.offset}px; margin-top:-${this.offset}px;`;
- break;
- case 'left':
- hovercardLeft = targetLeft - thisRect.width - this.offset;
- hovercardTop = targetTop + (targetRect.height - thisRect.height) / 2;
- cssText +=
- `padding-right:${this.offset}px; margin-right:-${this.offset}px;`;
- break;
- case 'right':
- hovercardLeft = targetRect.right + this.offset;
- hovercardTop = targetTop + (targetRect.height - thisRect.height) / 2;
- cssText +=
- `padding-left:${this.offset}px; margin-left:-${this.offset}px;`;
- break;
- case 'bottom-right':
- hovercardLeft = targetRect.left + targetRect.width + this.offset;
- hovercardTop = targetRect.top + targetRect.height + this.offset;
- cssText += `padding-top:${diagonalPadding}px;`;
- cssText += `padding-left:${diagonalPadding}px;`;
- cssText += `margin-left:-${diagonalPadding}px;`;
- cssText += `margin-top:-${diagonalPadding}px;`;
- break;
- case 'bottom-left':
- hovercardLeft = targetRect.left - thisRect.width - this.offset;
- hovercardTop = targetRect.top + targetRect.height + this.offset;
- cssText += `padding-top:${diagonalPadding}px;`;
- cssText += `padding-right:${diagonalPadding}px;`;
- cssText += `margin-right:-${diagonalPadding}px;`;
- cssText += `margin-top:-${diagonalPadding}px;`;
- break;
- case 'top-left':
- hovercardLeft = targetRect.left - thisRect.width - this.offset;
- hovercardTop = targetRect.top - thisRect.height - this.offset;
- cssText += `padding-bottom:${diagonalPadding}px;`;
- cssText += `padding-right:${diagonalPadding}px;`;
- cssText += `margin-bottom:-${diagonalPadding}px;`;
- cssText += `margin-right:-${diagonalPadding}px;`;
- break;
- case 'top-right':
- hovercardLeft = targetRect.left + targetRect.width + this.offset;
- hovercardTop = targetRect.top - thisRect.height - this.offset;
- cssText += `padding-bottom:${diagonalPadding}px;`;
- cssText += `padding-left:${diagonalPadding}px;`;
- cssText += `margin-bottom:-${diagonalPadding}px;`;
- cssText += `margin-left:-${diagonalPadding}px;`;
- break;
- }
-
- // Prevent hovercard from appearing outside the viewport.
- // TODO(kaspern): fix hovercard appearing outside viewport on bottom and
- // right.
- if (hovercardLeft < 0) { hovercardLeft = 0; }
- if (hovercardTop < 0) { hovercardTop = 0; }
- // Set the hovercard's position
- cssText += `left:${hovercardLeft}px; top:${hovercardTop}px;`;
- this.style.cssText = cssText;
- }
-
- /**
- * Responds to a change in the `for` value and gets the updated `target`
- * element for the hovercard.
- *
- * @private
- */
- _forChanged() {
- this._target = this.target;
- }
}
customElements.define(GrHovercard.is, GrHovercard);
diff --git a/polygerrit-ui/app/elements/shared/gr-hovercard/gr-hovercard_html.js b/polygerrit-ui/app/elements/shared/gr-hovercard/gr-hovercard_html.js
index 2969bdb..69fd4c5 100644
--- a/polygerrit-ui/app/elements/shared/gr-hovercard/gr-hovercard_html.js
+++ b/polygerrit-ui/app/elements/shared/gr-hovercard/gr-hovercard_html.js
@@ -17,26 +17,12 @@
import {html} from '@polymer/polymer/lib/utils/html-tag.js';
export const htmlTemplate = html`
- <style include="shared-styles">
- :host {
- box-sizing: border-box;
- opacity: 0;
- position: absolute;
- transition: opacity 200ms;
- visibility: hidden;
- z-index: 100;
- }
- :host(.hovered) {
- visibility: visible;
- opacity: 1;
- }
- #hovercard {
- background: var(--dialog-background-color);
- box-shadow: var(--elevation-level-2);
+ <style include="gr-hovercard-shared-style">
+ #container {
padding: var(--spacing-l);
}
</style>
- <div id="hovercard" role="tooltip" tabindex="-1">
+ <div id="container" role="tooltip" tabindex="-1">
<slot></slot>
</div>
`;
diff --git a/polygerrit-ui/app/elements/shared/gr-icons/gr-icons.js b/polygerrit-ui/app/elements/shared/gr-icons/gr-icons.js
index 5d7da6c..f7b8b63 100644
--- a/polygerrit-ui/app/elements/shared/gr-icons/gr-icons.js
+++ b/polygerrit-ui/app/elements/shared/gr-icons/gr-icons.js
@@ -59,6 +59,8 @@
<g id="hourglass"><path d="M6 2v6h.01L6 8.01 10 12l-4 4 .01.01H6V22h12v-5.99h-.01L18 16l-4-4 4-3.99-.01-.01H18V2H6z"></path><path d="M0 0h24v24H0V0z" fill="none"></path></g>
<!-- This SVG is a copy from material.io https://material.io/icons/#mode_comment-->
<g id="comment"><path d="M21.99 4c0-1.1-.89-2-1.99-2H4c-1.1 0-2 .9-2 2v12c0 1.1.9 2 2 2h14l4 4-.01-18z"></path><path d="M0 0h24v24H0z" fill="none"></path></g>
+ <!-- This SVG is a copy from material.io https://material.io/icons/#calendar_today-->
+ <g id="calendar"><path d="M20 3h-1V1h-2v2H7V1H5v2H4c-1.1 0-2 .9-2 2v16c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm0 18H4V8h16v13z"></path><path d="M0 0h24v24H0z" fill="none"></path></g>
<!-- This SVG is a copy from iron-icons https://github.com/PolymerElements/iron-icons/blob/master/iron-icons.js -->
<g id="error"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1 15h-2v-2h2v2zm0-4h-2V7h2v6z"></path></g>
<!-- This SVG is a copy from iron-icons https://github.com/PolymerElements/iron-icons/blob/master/iron-icons.js -->
@@ -94,51 +96,10 @@
<g id="review"><path d="M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z"></path></g>
<!-- This is a custom PolyGerrit SVG -->
<g id="zeroState"><path d="M22 9V7h-2V5c0-1.1-.9-2-2-2H4c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2v-2h2v-2h-2v-2h2v-2h-2V9h2zm-4 10H4V5h14v14zM6 13h5v4H6zm6-6h4v3h-4zM6 7h5v5H6zm6 4h4v6h-4z"></path></g>
+ <!-- This SVG is an adaptation of material.io https://material.io/icons/#label_important-->
+ <g id="attention"><path d="M5.5 19 l9 0 c.67 0 1.27 -.33 1.63 -.84 L20.5 12 l-4.37 -6.16 c-.36 -.51 -.96 -.84 -1.63 -.84 l-9 0 L9 12 z"></path></g>
</defs>
</svg>
</iron-iconset-svg>`;
document.head.appendChild($_documentContainer.content);
-
-/* This SVG is a copy from iron-icons https://github.com/PolymerElements/iron-icons/blob/master/iron-icons.js */
-/* This SVG is a copy from iron-icons https://github.com/PolymerElements/iron-icons/blob/master/iron-icons.js */
-/* This SVG is a copy from material.io https://material.io/icons/#unfold_more */
-/* This SVG is a copy from iron-icons https://github.com/PolymerElements/iron-icons/blob/master/iron-icons.js */
-/* This SVG is a copy from iron-icons https://github.com/PolymerElements/iron-icons/blob/master/iron-icons.js */
-/* This SVG is a copy from iron-icons https://github.com/PolymerElements/iron-icons/blob/master/iron-icons.js */
-/* This SVG is a copy from iron-icons https://github.com/PolymerElements/iron-icons/blob/master/iron-icons.js */
-/* This SVG is a copy from iron-icons https://github.com/PolymerElements/iron-icons/blob/master/iron-icons.js */
-/* This SVG is a copy from iron-icons https://github.com/PolymerElements/iron-icons/blob/master/iron-icons.js */
-/* This SVG is a copy from iron-icons https://github.com/PolymerElements/iron-icons/blob/master/iron-icons.js */
-/* This SVG is a copy from iron-icons https://github.com/PolymerElements/iron-icons/blob/master/iron-icons.js */
-/* This SVG is a copy from iron-icons https://github.com/PolymerElements/iron-icons/blob/master/iron-icons.js */
-/* This SVG is a copy from iron-icons https://github.com/PolymerElements/iron-icons/blob/master/iron-icons.js */
-/* This SVG is a copy from iron-icons https://github.com/PolymerElements/iron-icons/blob/master/editor-icons.html */
-/* This SVG is a copy from iron-icons https://github.com/PolymerElements/iron-icons/blob/master/editor-icons.html */
-/* This SVG is a copy from iron-icons https://github.com/PolymerElements/iron-icons/blob/master/iron-icons.js */
-/* This SVG is a copy from iron-icons https://github.com/PolymerElements/iron-icons/blob/master/iron-icons.js */
-/* This SVG is a copy from material.io https://material.io/icons/#ic_hourglass_full*/
-/* This SVG is a copy from material.io https://material.io/icons/#mode_comment*/
-/* This SVG is a copy from iron-icons https://github.com/PolymerElements/iron-icons/blob/master/iron-icons.js */
-/* This SVG is a copy from iron-icons https://github.com/PolymerElements/iron-icons/blob/master/iron-icons.js */
-/* This is a custom PolyGerrit SVG */
-/* This is a custom PolyGerrit SVG */
-/* This SVG is a copy from iron-icons https://github.com/PolymerElements/iron-icons/blob/master/iron-icons.js */
-/* This is a custom PolyGerrit SVG */
-/* This is a custom PolyGerrit SVG */
-/* This is a custom PolyGerrit SVG */
-/* This is a custom PolyGerrit SVG */
-/* This is a custom PolyGerrit SVG */
-/* This is a custom PolyGerrit SVG */
-/* This is a custom PolyGerrit SVG */
-/* This is a custom PolyGerrit SVG */
-/* This is a custom PolyGerrit SVG */
-/* This is a custom PolyGerrit SVG */
-/* This is a custom PolyGerrit SVG */
-/* This is a custom PolyGerrit SVG */
-/*
- FIXME(polymer-modulizer): the above comments were extracted
- from HTML and may be out of place here. Review them and
- then delete this comment!
-*/
-
diff --git a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.js b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.js
index 3e78bd3..fea44b4 100644
--- a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.js
+++ b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.js
@@ -772,6 +772,23 @@
}
/**
+ * @param {string} displayName
+ * @param {function(?Response, string=)=} opt_errFn
+ */
+ setAccountDisplayName(displayName, opt_errFn) {
+ const req = {
+ method: 'PUT',
+ url: '/accounts/self/displayname',
+ body: {display_name: displayName},
+ errFn: opt_errFn,
+ parseResponse: true,
+ reportUrlAsIs: true,
+ };
+ return this._restApiHelper.send(req)
+ .then(newName => this._updateCachedAccount({displayName: newName}));
+ }
+
+ /**
* @param {string} status
* @param {function(?Response, string=)=} opt_errFn
*/
diff --git a/polygerrit-ui/app/rules.bzl b/polygerrit-ui/app/rules.bzl
index 8307fad..6e1f63a 100644
--- a/polygerrit-ui/app/rules.bzl
+++ b/polygerrit-ui/app/rules.bzl
@@ -1,5 +1,4 @@
load("//tools/bzl:genrule2.bzl", "genrule2")
-load("//tools/node_tools/legacy:index.bzl", "polymer_bundler_tool")
load("@npm_bazel_rollup//:index.bzl", "rollup_bundle")
def polygerrit_bundle(name, srcs, outs, entry_point):
diff --git a/polygerrit-ui/app/scripts/gr-display-name-utils/gr-display-name-utils.js b/polygerrit-ui/app/scripts/gr-display-name-utils/gr-display-name-utils.js
index f0d0e7f..cefd254 100644
--- a/polygerrit-ui/app/scripts/gr-display-name-utils/gr-display-name-utils.js
+++ b/polygerrit-ui/app/scripts/gr-display-name-utils/gr-display-name-utils.js
@@ -24,16 +24,12 @@
const ANONYMOUS_NAME = 'Anonymous';
class GrDisplayNameUtils {
- /**
- * enableEmail when true enables to fallback to using email if
- * the account name is not avilable.
- */
- static getUserName(config, account, enableEmail) {
+ static getUserName(config, account) {
if (account && account.name) {
return account.name;
} else if (account && account.username) {
return account.username;
- } else if (enableEmail && account && account.email) {
+ } else if (account && account.email) {
return account.email;
} else if (config && config.user &&
config.user.anonymous_coward_name !== 'Anonymous Coward') {
@@ -43,8 +39,8 @@
return ANONYMOUS_NAME;
}
- static getAccountDisplayName(config, account, enableEmail) {
- const reviewerName = this.getUserName(config, account, !!enableEmail);
+ static getAccountDisplayName(config, account) {
+ const reviewerName = this.getUserName(config, account);
const reviewerEmail = this._accountEmail(account.email);
const reviewerStatus = account.status ? '(' + account.status + ')' : '';
return [reviewerName, reviewerEmail, reviewerStatus]
diff --git a/polygerrit-ui/app/scripts/gr-display-name-utils/gr-display-name-utils_test.html b/polygerrit-ui/app/scripts/gr-display-name-utils/gr-display-name-utils_test.html
index b9ddac62..7ef5250 100644
--- a/polygerrit-ui/app/scripts/gr-display-name-utils/gr-display-name-utils_test.html
+++ b/polygerrit-ui/app/scripts/gr-display-name-utils/gr-display-name-utils_test.html
@@ -43,7 +43,7 @@
const account = {
name: 'test-name',
};
- assert.deepEqual(GrDisplayNameUtils.getUserName(config, account, true),
+ assert.deepEqual(GrDisplayNameUtils.getUserName(config, account),
'test-name');
});
@@ -51,7 +51,7 @@
const account = {
username: 'test-user',
};
- assert.deepEqual(GrDisplayNameUtils.getUserName(config, account, true),
+ assert.deepEqual(GrDisplayNameUtils.getUserName(config, account),
'test-user');
});
@@ -59,12 +59,12 @@
const account = {
email: 'test-user@test-url.com',
};
- assert.deepEqual(GrDisplayNameUtils.getUserName(config, account, true),
+ assert.deepEqual(GrDisplayNameUtils.getUserName(config, account),
'test-user@test-url.com');
});
test('getUserName returns not Anonymous Coward as the anon name', () => {
- assert.deepEqual(GrDisplayNameUtils.getUserName(config, null, true),
+ assert.deepEqual(GrDisplayNameUtils.getUserName(config, null),
'Anonymous');
});
@@ -74,7 +74,7 @@
anonymous_coward_name: 'Test Anon',
},
};
- assert.deepEqual(GrDisplayNameUtils.getUserName(config, null, true),
+ assert.deepEqual(GrDisplayNameUtils.getUserName(config, null),
'Test Anon');
});
@@ -89,13 +89,6 @@
assert.equal(
GrDisplayNameUtils.getAccountDisplayName(config,
{email: 'my@example.com'}),
- 'Anonymous <my@example.com>');
- });
-
- test('getAccountDisplayName - account with email only - allowEmail', () => {
- assert.equal(
- GrDisplayNameUtils.getAccountDisplayName(config,
- {email: 'my@example.com'}, true),
'my@example.com <my@example.com>');
});
diff --git a/polygerrit-ui/app/scripts/gr-email-suggestions-provider/gr-email-suggestions-provider.js b/polygerrit-ui/app/scripts/gr-email-suggestions-provider/gr-email-suggestions-provider.js
index 67001d2..a1fd94a 100644
--- a/polygerrit-ui/app/scripts/gr-email-suggestions-provider/gr-email-suggestions-provider.js
+++ b/polygerrit-ui/app/scripts/gr-email-suggestions-provider/gr-email-suggestions-provider.js
@@ -36,7 +36,7 @@
makeSuggestionItem(account) {
return {
- name: GrDisplayNameUtils.getAccountDisplayName(null, account, true),
+ name: GrDisplayNameUtils.getAccountDisplayName(null, account),
value: {account, count: 1},
};
}
diff --git a/polygerrit-ui/app/scripts/gr-reviewer-suggestions-provider/gr-reviewer-suggestions-provider.js b/polygerrit-ui/app/scripts/gr-reviewer-suggestions-provider/gr-reviewer-suggestions-provider.js
index fecf75aa..a47eb72 100644
--- a/polygerrit-ui/app/scripts/gr-reviewer-suggestions-provider/gr-reviewer-suggestions-provider.js
+++ b/polygerrit-ui/app/scripts/gr-reviewer-suggestions-provider/gr-reviewer-suggestions-provider.js
@@ -87,7 +87,7 @@
// Reviewer is an account suggestion from getChangeSuggestedReviewers.
return {
name: GrDisplayNameUtils.getAccountDisplayName(this._config,
- suggestion.account, false),
+ suggestion.account),
value: suggestion,
};
}
@@ -104,7 +104,7 @@
// Reviewer is an account suggestion from getSuggestedAccounts.
return {
name: GrDisplayNameUtils.getAccountDisplayName(this._config,
- suggestion, false),
+ suggestion),
value: {account: suggestion, count: 1},
};
}
diff --git a/polygerrit-ui/app/scripts/util.js b/polygerrit-ui/app/scripts/util.js
index 6a8a116..e8a1d21 100644
--- a/polygerrit-ui/app/scripts/util.js
+++ b/polygerrit-ui/app/scripts/util.js
@@ -170,5 +170,51 @@
return [...results];
};
+ function getPathFromNode(el) {
+ if (!el.tagName || el.tagName === 'GR-APP'
+ || el instanceof DocumentFragment
+ || el instanceof HTMLSlotElement) {
+ return '';
+ }
+ let path = el.tagName.toLowerCase();
+ if (el.id) path += `#${el.id}`;
+ if (el.className) path += `.${el.className.replace(/ /g, '.')}`;
+ return path;
+ }
+
+ /**
+ * Retrieves the dom path of the current event.
+ *
+ * If the event object contains a `path` property, then use it,
+ * otherwise, construct the dom path based on the event target.
+ *
+ * @param {!Event} e
+ * @return {string}
+ * @example
+ *
+ * domNode.onclick = e => {
+ * getEventPath(e); // eg: div.class1>p#pid.class2
+ * }
+ */
+ util.getEventPath = e => {
+ if (!e) return '';
+
+ let path = e.path;
+ if (!path || !path.length) {
+ path = [];
+ let el = e.target;
+ while (el) {
+ path.push(el);
+ el = el.parentNode || el.host;
+ }
+ }
+
+ return path.reduce((domPath, curEl) => {
+ const pathForEl = getPathFromNode(curEl);
+ if (!pathForEl) return domPath;
+ return domPath ? `${pathForEl}>${domPath}` : pathForEl;
+ }, '');
+ };
+
window.util = util;
})(window);
diff --git a/polygerrit-ui/app/scripts/util_test.html b/polygerrit-ui/app/scripts/util_test.html
new file mode 100644
index 0000000..332707e
--- /dev/null
+++ b/polygerrit-ui/app/scripts/util_test.html
@@ -0,0 +1,88 @@
+<!DOCTYPE html>
+<!--
+@license
+Copyright (C) 2020 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+
+<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
+<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/components/wct-browser-legacy/browser.js"></script>
+
+<test-fixture id="basic">
+ <template>
+ <div id="test" class="a b c">
+ <a class="testBtn"></a>
+ </div>
+ </template>
+</test-fixture>
+
+<script type="module">
+ import '../test/common-test-setup.js';
+ import './util.js';
+ suite('util tests', () => {
+ suite('getEventPath', () => {
+ test('empty event', () => {
+ assert.equal(util.getEventPath(), '');
+ assert.equal(util.getEventPath(null), '');
+ assert.equal(util.getEventPath(undefined), '');
+ assert.equal(util.getEventPath({}), '');
+ });
+
+ test('event with fake path', () => {
+ assert.equal(util.getEventPath({path: []}), '');
+ assert.equal(util.getEventPath({path: [
+ {tagName: 'dd'},
+ ]}), 'dd');
+ });
+
+ test('event with fake complicated path', () => {
+ assert.equal(util.getEventPath({path: [
+ {tagName: 'dd', id: 'test', className: 'a b'},
+ {tagName: 'DIV', id: 'test2', className: 'a b c'},
+ ]}), 'div#test2.a.b.c>dd#test.a.b');
+ });
+
+ test('event with fake target', () => {
+ const fakeTargetParent2 = {
+ tagName: 'DIV', id: 'test2', className: 'a b c',
+ };
+ const fakeTargetParent1 = {
+ parentNode: fakeTargetParent2,
+ tagName: 'dd',
+ id: 'test',
+ className: 'a b',
+ };
+ const fakeTarget = {tagName: 'SPAN', parentNode: fakeTargetParent1};
+ assert.equal(
+ util.getEventPath({target: fakeTarget}),
+ 'div#test2.a.b.c>dd#test.a.b>span'
+ );
+ });
+
+ test('event with real click', () => {
+ const element = fixture('basic');
+ const aLink = element.querySelector('a');
+ let path;
+ aLink.onclick = e => path = util.getEventPath(e);
+ MockInteractions.click(aLink);
+ assert.equal(
+ path,
+ 'html>body>test-fixture#basic>div#test.a.b.c>a.testBtn'
+ );
+ });
+ });
+ });
+</script>
\ No newline at end of file
diff --git a/polygerrit-ui/app/test/index.html b/polygerrit-ui/app/test/index.html
index 45e5af6..5b19340 100644
--- a/polygerrit-ui/app/test/index.html
+++ b/polygerrit-ui/app/test/index.html
@@ -189,6 +189,7 @@
'shared/gr-editable-label/gr-editable-label_test.html',
'shared/gr-formatted-text/gr-formatted-text_test.html',
'shared/gr-hovercard/gr-hovercard_test.html',
+ 'shared/gr-hovercard-account/gr-hovercard-account_test.html',
'shared/gr-js-api-interface/gr-annotation-actions-context_test.html',
'shared/gr-js-api-interface/gr-annotation-actions-js-api_test.html',
'shared/gr-js-api-interface/gr-change-actions-js-api_test.html',
@@ -262,6 +263,7 @@
'gr-group-suggestions-provider/gr-group-suggestions-provider_test.html',
'gr-display-name-utils/gr-display-name-utils_test.html',
'gr-email-suggestions-provider/gr-email-suggestions-provider_test.html',
+ 'util_test.html',
];
/* eslint-enable max-len */
for (let file of scripts) {
diff --git a/polygerrit-ui/server.go b/polygerrit-ui/server.go
index fde2f42..339812b 100644
--- a/polygerrit-ui/server.go
+++ b/polygerrit-ui/server.go
@@ -115,16 +115,16 @@
return
}
- requestPath := parsedUrl.Path
+ normalizedContentPath := parsedUrl.Path
- if !strings.HasPrefix(requestPath, "/") {
- requestPath = "/" + requestPath
+ if !strings.HasPrefix(normalizedContentPath, "/") {
+ normalizedContentPath = "/" + normalizedContentPath
}
- isJsFile := strings.HasSuffix(requestPath, ".js") || strings.HasSuffix(requestPath, ".mjs")
- data, err := readFile(parsedUrl.Path, requestPath)
+ isJsFile := strings.HasSuffix(normalizedContentPath, ".js") || strings.HasSuffix(normalizedContentPath, ".mjs")
+ data, err := getContent(normalizedContentPath)
if err != nil {
- data, err = readFile(parsedUrl.Path + ".js", requestPath + ".js")
+ data, err = getContent(normalizedContentPath + ".js")
if err != nil {
writer.WriteHeader(404)
return
@@ -135,13 +135,13 @@
moduleImportRegexp := regexp.MustCompile("(?m)^(import.*)'([^/.].*)';$")
data = moduleImportRegexp.ReplaceAll(data, []byte("$1 '/node_modules/$2';"))
writer.Header().Set("Content-Type", "application/javascript")
- } else if strings.HasSuffix(requestPath, ".css") {
+ } else if strings.HasSuffix(normalizedContentPath, ".css") {
writer.Header().Set("Content-Type", "text/css")
- } else if strings.HasSuffix(requestPath, "_test.html") {
+ } else if strings.HasSuffix(normalizedContentPath, "_test.html") {
moduleImportRegexp := regexp.MustCompile("(?m)^(import.*)'([^/.].*)';$")
data = moduleImportRegexp.ReplaceAll(data, []byte("$1 '/node_modules/$2';"))
writer.Header().Set("Content-Type", "text/html")
- } else if strings.HasSuffix(requestPath, ".html") {
+ } else if strings.HasSuffix(normalizedContentPath, ".html") {
writer.Header().Set("Content-Type", "text/html")
}
writer.WriteHeader(200)
@@ -149,23 +149,24 @@
writer.Write(data)
}
-func readFile(originalPath string, redirectedPath string) ([]byte, error) {
- pathsToTry := []string{"app" + redirectedPath}
+func getContent(normalizedContentPath string) ([]byte, error) {
+ //normalizedContentPath must always starts with '/'
+ pathsToTry := []string{"app" + normalizedContentPath}
bowerComponentsSuffix := "/bower_components/"
nodeModulesPrefix := "/node_modules/"
testComponentsPrefix := "/components/"
- if strings.HasPrefix(originalPath, testComponentsPrefix) {
- pathsToTry = append(pathsToTry, "node_modules/wct-browser-legacy/node_modules/"+originalPath[len(testComponentsPrefix):])
- pathsToTry = append(pathsToTry, "node_modules/"+originalPath[len(testComponentsPrefix):])
+ if strings.HasPrefix(normalizedContentPath, testComponentsPrefix) {
+ pathsToTry = append(pathsToTry, "node_modules/wct-browser-legacy/node_modules/"+normalizedContentPath[len(testComponentsPrefix):])
+ pathsToTry = append(pathsToTry, "node_modules/"+normalizedContentPath[len(testComponentsPrefix):])
}
- if strings.HasPrefix(originalPath, bowerComponentsSuffix) {
- pathsToTry = append(pathsToTry, "node_modules/@webcomponents/"+originalPath[len(bowerComponentsSuffix):])
+ if strings.HasPrefix(normalizedContentPath, bowerComponentsSuffix) {
+ pathsToTry = append(pathsToTry, "node_modules/@webcomponents/"+normalizedContentPath[len(bowerComponentsSuffix):])
}
- if strings.HasPrefix(originalPath, nodeModulesPrefix) {
- pathsToTry = append(pathsToTry, "node_modules/"+originalPath[len(nodeModulesPrefix):])
+ if strings.HasPrefix(normalizedContentPath, nodeModulesPrefix) {
+ pathsToTry = append(pathsToTry, "node_modules/"+normalizedContentPath[len(nodeModulesPrefix):])
}
for _, path := range pathsToTry {
diff --git a/proto/cache.proto b/proto/cache.proto
index 5fc5e68..c80d51b 100644
--- a/proto/cache.proto
+++ b/proto/cache.proto
@@ -208,17 +208,17 @@
}
repeated AssigneeStatusUpdateProto assignee_update = 22;
- // An update to the attention set of the change. See class AttentionStatus for
- // context.
- message AttentionStatusProto {
+ // An update to the attention set of the change. See class AttentionSetUpdate
+ // for context.
+ message AttentionSetUpdateProto {
// Epoch millis.
int64 timestamp_millis = 1;
int32 account = 2;
- // Maps to enum AttentionStatus.Operation
+ // Maps to enum AttentionSetUpdate.Operation
string operation = 3;
string reason = 4;
}
- repeated AttentionStatusProto attention_status = 23;
+ repeated AttentionSetUpdateProto attention_set_update = 23;
}
// Serialized form of com.google.gerrit.server.query.change.ConflictKey