Merge changes Ib3f0849f,I9d12ed46,I38520cff
* changes:
Factor out Contributor Agreements from ProjectControl
Add ProjectPermissions for upload and receive pack, migrate callers
Add ProjectPermission.READ_NO_CONFIG
diff --git a/Documentation/config-gerrit.txt b/Documentation/config-gerrit.txt
index 6e1f394..93910d9 100644
--- a/Documentation/config-gerrit.txt
+++ b/Documentation/config-gerrit.txt
@@ -628,6 +628,23 @@
+
By default, true.
+[[auth.autoUpdateAccountActiveStatus]]auth.autoUpdateAccountActiveStatus::
++
+Whether to allow automatic synchronization of an account's inactive flag upon login.
+If set to true, upon login, if the authentication back-end reports the account as active,
+the account's inactive flag in the internal Gerrit database will be updated to be active.
+If the authentication back-end reports the account as inactive, the account's flag will be
+updated to be inactive and the login attempt will be blocked. Users enabling this feature
+should ensure that their authentication back-end is supported. Currently, only
+strict 'LDAP' authentication is supported.
++
+In addition, if this parameter is not set, or false, the corresponding scheduled
+task to deactivate inactive Gerrit accounts will also be disabled. If this
+parameter is set to true, users should also consider configuring the
+link:#accountDeactivation[accountDeactivation] section appropriately.
++
+By default, false.
+
[[cache]]
=== Section cache
@@ -4538,6 +4555,44 @@
If no groups are added, any user will be allowed to execute
'upload-pack' on the server.
+[[accountDeactivation]]
+=== Section accountDeactivation
+
+Configures the parameters for the scheduled task to sweep and deactivate Gerrit
+accounts according to their status reported by the auth backend. Currently only
+supported for LDAP backends.
+
+[[accountDeactivation.startTime]]accountDeactivation.startTime::
++
+Start time to define the first execution of account deactivations.
+If the configured `'accountDeactivation.interval'` is shorter than `'accountDeactivation.startTime - now'`
+the start time will be preponed by the maximum integral multiple of
+`'accountDeactivation.interval'` so that the start time is still in the future.
++
+----
+<day of week> <hours>:<minutes>
+or
+<hours>:<minutes>
+
+<day of week> : Mon, Tue, Wed, Thu, Fri, Sat, Sun
+<hours> : 00-23
+<minutes> : 0-59
+----
+
+[[accountDeactivation.interval]]accountDeactivation.interval::
++
+Interval for periodic repetition of triggering account deactivation sweeps.
+The interval must be larger than zero. The following suffixes are supported
+to define the time unit for the interval:
++
+* `s, sec, second, seconds`
+* `m, min, minute, minutes`
+* `h, hr, hour, hours`
+* `d, day, days`
+* `w, week, weeks` (`1 week` is treated as `7 days`)
+* `mon, month, months` (`1 month` is treated as `30 days`)
+* `y, year, years` (`1 year` is treated as `365 days`)
+
[[urlAlias]]
=== Section urlAlias
diff --git a/Documentation/dev-stars.txt b/Documentation/dev-stars.txt
index 1fb871a..a83ad44 100644
--- a/Documentation/dev-stars.txt
+++ b/Documentation/dev-stars.txt
@@ -61,18 +61,24 @@
The ignore star is represented by the special star label 'ignore'.
-[[mute-star]]
-== Mute Star
+[[reviewed-star]]
+== Reviewed Star
-If the "mute/<patchset_id>"-star is set by a user, and <patchset_id>
+If the "reviewed/<patchset_id>"-star is set by a user, and <patchset_id>
matches the current patch set, the change is always reported as "reviewed"
in the ChangeInfo.
This allows users to "de-highlight" changes in a dashboard until a new
patchset has been uploaded.
-The ChangeInfo muted-field will show if the change is currently in a
-mute state.
+[[unreviewed-star]]
+== Unreviewed Star
+
+If the "unreviewed/<patchset_id>"-star is set by a user, and <patchset_id>
+matches the current patch set, the change is always reported as "unreviewed"
+in the ChangeInfo.
+
+This allows users to "highlight" changes in a dashboard.
[[query-stars]]
== Query Stars
diff --git a/Documentation/linux-quickstart.txt b/Documentation/linux-quickstart.txt
index da81dff..6b9374f 100644
--- a/Documentation/linux-quickstart.txt
+++ b/Documentation/linux-quickstart.txt
@@ -73,7 +73,7 @@
== Update the listen URL
Another recommended task is to change the URL that Gerrit listens to from `*`
-to `localhost`. This changes helps prevent outside connections from contacting
+to `localhost`. This change helps prevent outside connections from contacting
the instance.
....
diff --git a/Documentation/rest-api-changes.txt b/Documentation/rest-api-changes.txt
index fa91c2d..50afe40 100644
--- a/Documentation/rest-api-changes.txt
+++ b/Documentation/rest-api-changes.txt
@@ -2329,13 +2329,13 @@
PUT /changes/myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940/unignore HTTP/1.0
----
-[[mute]]
-=== Mute
+[[mark-as-reviewed]]
+=== Mark as Reviewed
--
-'PUT /changes/link:#change-id[\{change-id\}]/mute'
+'PUT /changes/link:#change-id[\{change-id\}]/reviewed'
--
-Marks a change as muted.
+Marks a change as reviewed.
This allows users to "de-highlight" changes in their dashboard until a new
patch set is uploaded.
@@ -2347,20 +2347,22 @@
.Request
----
- PUT /changes/myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940/mute HTTP/1.0
+ PUT /changes/myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940/reviewed HTTP/1.0
----
-[[unmute]]
-=== Unmute
+[[mark-as-unreviewed]]
+=== Mark as Unreviewed
--
-'PUT /changes/link:#change-id[\{change-id\}]/unmute'
+'PUT /changes/link:#change-id[\{change-id\}]/unreviewed'
--
-Unmutes a change.
+Marks a change as unreviewed.
+
+This allows users to "highlight" changes in their dashboard
.Request
----
- PUT /changes/myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940/unmute HTTP/1.0
+ PUT /changes/myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940/unreviewed HTTP/1.0
----
[[get-hashtags]]
@@ -5641,8 +5643,6 @@
change. The labels are lexicographically sorted.
|`reviewed` |not set if `false`|
Whether the change was reviewed by the calling user.
-|`muted` |not set if `false`|
-Whether the change has been link:#mute[muted] by the calling user.
Only set if link:#reviewed[reviewed] is requested.
|`submit_type` |optional|
The link:project-configuration.html#submit_type[submit type] of the change. +
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/change/ChangeIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/change/ChangeIT.java
index 65e5909..c2d3184 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/change/ChangeIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/change/ChangeIT.java
@@ -3374,7 +3374,7 @@
}
@Test
- public void mute() throws Exception {
+ public void markAsReviewed() throws Exception {
TestAccount user2 = accountCreator.user2();
PushOneCommit.Result r = createChange();
@@ -3384,16 +3384,16 @@
gApi.changes().id(r.getChangeId()).addReviewer(in);
setApiUser(user);
- assertThat(gApi.changes().id(r.getChangeId()).muted()).isFalse();
- gApi.changes().id(r.getChangeId()).mute(true);
- assertThat(gApi.changes().id(r.getChangeId()).muted()).isTrue();
+ assertThat(gApi.changes().id(r.getChangeId()).get().reviewed).isNull();
+ gApi.changes().id(r.getChangeId()).markAsReviewed(true);
+ assertThat(gApi.changes().id(r.getChangeId()).get().reviewed).isTrue();
setApiUser(user2);
sender.clear();
amendChange(r.getChangeId());
setApiUser(user);
- assertThat(gApi.changes().id(r.getChangeId()).muted()).isFalse();
+ assertThat(gApi.changes().id(r.getChangeId()).get().reviewed).isNull();
List<Message> messages = sender.getMessages();
assertThat(messages).hasSize(1);
@@ -3401,12 +3401,73 @@
}
@Test
- public void cannotMuteOwnChange() throws Exception {
+ public void cannotSetUnreviewedLabelForPatchSetThatAlreadyHasReviewedLabel() throws Exception {
String changeId = createChange().getChangeId();
+ setApiUser(user);
+ gApi.changes().id(changeId).markAsReviewed(true);
+ assertThat(gApi.changes().id(changeId).get().reviewed).isTrue();
+
exception.expect(BadRequestException.class);
- exception.expectMessage("cannot mute own change");
- gApi.changes().id(changeId).mute(true);
+ exception.expectMessage(
+ "The labels "
+ + StarredChangesUtil.REVIEWED_LABEL
+ + "/"
+ + 1
+ + " and "
+ + StarredChangesUtil.UNREVIEWED_LABEL
+ + "/"
+ + 1
+ + " are mutually exclusive. Only one of them can be set.");
+ gApi.accounts()
+ .self()
+ .setStars(
+ changeId, new StarsInput(ImmutableSet.of(StarredChangesUtil.UNREVIEWED_LABEL + "/1")));
+ }
+
+ @Test
+ public void cannotSetReviewedLabelForPatchSetThatAlreadyHasUnreviewedLabel() throws Exception {
+ String changeId = createChange().getChangeId();
+
+ setApiUser(user);
+ gApi.changes().id(changeId).markAsReviewed(false);
+ assertThat(gApi.changes().id(changeId).get().reviewed).isNull();
+
+ exception.expect(BadRequestException.class);
+ exception.expectMessage(
+ "The labels "
+ + StarredChangesUtil.REVIEWED_LABEL
+ + "/"
+ + 1
+ + " and "
+ + StarredChangesUtil.UNREVIEWED_LABEL
+ + "/"
+ + 1
+ + " are mutually exclusive. Only one of them can be set.");
+ gApi.accounts()
+ .self()
+ .setStars(
+ changeId, new StarsInput(ImmutableSet.of(StarredChangesUtil.REVIEWED_LABEL + "/1")));
+ }
+
+ @Test
+ public void setReviewedAndUnreviewedLabelsForDifferentPatchSets() throws Exception {
+ String changeId = createChange().getChangeId();
+
+ setApiUser(user);
+ gApi.changes().id(changeId).markAsReviewed(true);
+ assertThat(gApi.changes().id(changeId).get().reviewed).isTrue();
+
+ amendChange(changeId);
+ assertThat(gApi.changes().id(changeId).get().reviewed).isNull();
+
+ gApi.changes().id(changeId).markAsReviewed(false);
+ assertThat(gApi.changes().id(changeId).get().reviewed).isNull();
+
+ assertThat(gApi.accounts().self().getStars(changeId))
+ .containsExactly(
+ StarredChangesUtil.REVIEWED_LABEL + "/" + 1,
+ StarredChangesUtil.UNREVIEWED_LABEL + "/" + 2);
}
@Test
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/project/DashboardIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/project/DashboardIT.java
index bfe6b8b..5cdb583 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/project/DashboardIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/project/DashboardIT.java
@@ -14,13 +14,32 @@
package com.google.gerrit.acceptance.api.project;
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
+
import com.google.gerrit.acceptance.AbstractDaemonTest;
import com.google.gerrit.acceptance.NoHttpd;
+import com.google.gerrit.common.data.Permission;
+import com.google.gerrit.extensions.api.projects.BranchInput;
+import com.google.gerrit.extensions.api.projects.DashboardInfo;
+import com.google.gerrit.extensions.restapi.BadRequestException;
+import com.google.gerrit.extensions.restapi.ResourceConflictException;
import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
+import com.google.gerrit.server.project.DashboardsCollection;
+import java.util.List;
+import org.eclipse.jgit.junit.TestRepository;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.junit.Before;
import org.junit.Test;
@NoHttpd
public class DashboardIT extends AbstractDaemonTest {
+ @Before
+ public void setup() throws Exception {
+ allow("refs/meta/dashboards/*", Permission.CREATE, REGISTERED_USERS);
+ }
+
@Test
public void defaultDashboardDoesNotExist() throws Exception {
exception.expect(ResourceNotFoundException.class);
@@ -32,4 +51,81 @@
exception.expect(ResourceNotFoundException.class);
gApi.projects().name(project.get()).dashboard("my:dashboard").get();
}
+
+ @Test
+ public void getDashboard() throws Exception {
+ assertThat(dashboards()).isEmpty();
+ DashboardInfo info = createDashboard(DashboardsCollection.DEFAULT_DASHBOARD_NAME, "test");
+ DashboardInfo result = gApi.projects().name(project.get()).dashboard(info.id).get();
+ assertThat(result.id).isEqualTo(info.id);
+ assertThat(result.path).isEqualTo(info.path);
+ assertThat(result.ref).isEqualTo(info.ref);
+ assertThat(result.project).isEqualTo(project.get());
+ assertThat(result.definingProject).isEqualTo(project.get());
+ assertThat(dashboards()).hasSize(1);
+ }
+
+ @Test
+ public void setDefaultDashboard() throws Exception {
+ DashboardInfo info = createDashboard(DashboardsCollection.DEFAULT_DASHBOARD_NAME, "test");
+ assertThat(info.isDefault).isNull();
+ gApi.projects().name(project.get()).dashboard(info.id).setDefault();
+ assertThat(gApi.projects().name(project.get()).dashboard(info.id).get().isDefault).isTrue();
+ assertThat(gApi.projects().name(project.get()).defaultDashboard().get().id).isEqualTo(info.id);
+ }
+
+ @Test
+ public void replaceDefaultDashboard() throws Exception {
+ DashboardInfo d1 = createDashboard(DashboardsCollection.DEFAULT_DASHBOARD_NAME, "test1");
+ DashboardInfo d2 = createDashboard(DashboardsCollection.DEFAULT_DASHBOARD_NAME, "test2");
+ assertThat(d1.isDefault).isNull();
+ assertThat(d2.isDefault).isNull();
+ gApi.projects().name(project.get()).dashboard(d1.id).setDefault();
+ assertThat(gApi.projects().name(project.get()).dashboard(d1.id).get().isDefault).isTrue();
+ assertThat(gApi.projects().name(project.get()).dashboard(d2.id).get().isDefault).isNull();
+ assertThat(gApi.projects().name(project.get()).defaultDashboard().get().id).isEqualTo(d1.id);
+ gApi.projects().name(project.get()).dashboard(d2.id).setDefault();
+ assertThat(gApi.projects().name(project.get()).defaultDashboard().get().id).isEqualTo(d2.id);
+ assertThat(gApi.projects().name(project.get()).dashboard(d1.id).get().isDefault).isNull();
+ assertThat(gApi.projects().name(project.get()).dashboard(d2.id).get().isDefault).isTrue();
+ }
+
+ @Test
+ public void cannotGetDashboardWithInheritedForNonDefault() throws Exception {
+ DashboardInfo info = createDashboard(DashboardsCollection.DEFAULT_DASHBOARD_NAME, "test");
+ exception.expect(BadRequestException.class);
+ exception.expectMessage("inherited flag can only be used with default");
+ gApi.projects().name(project.get()).dashboard(info.id).get(true);
+ }
+
+ private List<DashboardInfo> dashboards() throws Exception {
+ return gApi.projects().name(project.get()).dashboards().get();
+ }
+
+ private DashboardInfo createDashboard(String ref, String path) throws Exception {
+ DashboardInfo info = DashboardsCollection.newDashboardInfo(ref, path);
+ String canonicalRef = DashboardsCollection.normalizeDashboardRef(info.ref);
+ try {
+ gApi.projects().name(project.get()).branch(canonicalRef).create(new BranchInput());
+ } catch (ResourceConflictException e) {
+ // The branch already exists if this method has already been called once.
+ if (!e.getMessage().contains("already exists")) {
+ throw e;
+ }
+ }
+ try (Repository r = repoManager.openRepository(project)) {
+ TestRepository<Repository>.CommitBuilder cb =
+ new TestRepository<>(r).branch(canonicalRef).commit();
+ String content =
+ "[dashboard]\n"
+ + "Description = Test\n"
+ + "foreach = owner:self\n"
+ + "[section \"Mine\"]\n"
+ + "query = is:open";
+ cb.add(info.path, content);
+ RevCommit c = cb.create();
+ gApi.projects().name(project.get()).commit(c.name());
+ }
+ return info;
+ }
}
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/ChangeApi.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/ChangeApi.java
index 1fc04b4..481681e 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/ChangeApi.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/ChangeApi.java
@@ -119,18 +119,12 @@
boolean ignored() throws RestApiException;
/**
- * Mute or un-mute this change.
+ * Mark this change as reviewed/unreviewed.
*
- * @param mute mute the change if true
+ * @param reviewed flag to decide if this change should be marked as reviewed ({@code true}) or
+ * unreviewed ({@code false})
*/
- void mute(boolean mute) throws RestApiException;
-
- /**
- * Check if this change is muted.
- *
- * @return true if the change is muted.
- */
- boolean muted() throws RestApiException;
+ void markAsReviewed(boolean reviewed) throws RestApiException;
/**
* Create a new change that reverts this change.
@@ -583,12 +577,7 @@
}
@Override
- public void mute(boolean mute) throws RestApiException {
- throw new NotImplementedException();
- }
-
- @Override
- public boolean muted() throws RestApiException {
+ public void markAsReviewed(boolean reviewed) throws RestApiException {
throw new NotImplementedException();
}
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/DashboardApi.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/DashboardApi.java
index a411e0e..3cde570 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/DashboardApi.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/DashboardApi.java
@@ -23,6 +23,8 @@
DashboardInfo get(boolean inherited) throws RestApiException;
+ void setDefault() throws RestApiException;
+
/**
* A default implementation which allows source compatibility when adding new methods to the
* interface.
@@ -37,5 +39,10 @@
public DashboardInfo get(boolean inherited) throws RestApiException {
throw new NotImplementedException();
}
+
+ @Override
+ public void setDefault() throws RestApiException {
+ throw new NotImplementedException();
+ }
}
}
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/ProjectApi.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/ProjectApi.java
index a61b68a..3379edc 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/ProjectApi.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/ProjectApi.java
@@ -154,6 +154,12 @@
*/
DashboardApi defaultDashboard() throws RestApiException;
+ abstract class ListDashboardsRequest {
+ public abstract List<DashboardInfo> get() throws RestApiException;
+ }
+
+ ListDashboardsRequest dashboards() throws RestApiException;
+
/**
* A default implementation which allows source compatibility when adding new methods to the
* interface.
@@ -273,5 +279,10 @@
public DashboardApi defaultDashboard() throws RestApiException {
throw new NotImplementedException();
}
+
+ @Override
+ public ListDashboardsRequest dashboards() throws RestApiException {
+ throw new NotImplementedException();
+ }
}
}
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/ChangeInfo.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/ChangeInfo.java
index 706482f..f802049 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/ChangeInfo.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/ChangeInfo.java
@@ -37,7 +37,6 @@
public Timestamp submitted;
public AccountInfo submitter;
public Boolean starred;
- public Boolean muted;
public Collection<String> stars;
public Boolean reviewed;
public SubmitType submitType;
diff --git a/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/info/ChangeInfo.java b/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/info/ChangeInfo.java
index 5ef3dce..866d74f 100644
--- a/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/info/ChangeInfo.java
+++ b/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/info/ChangeInfo.java
@@ -138,8 +138,6 @@
public final native boolean starred() /*-{ return this.starred ? true : false; }-*/;
- public final native boolean muted() /*-{ return this.muted ? true : false; }-*/;
-
public final native boolean reviewed() /*-{ return this.reviewed ? true : false; }-*/;
public final native boolean isPrivate() /*-{ return this.is_private ? true : false; }-*/;
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/RunAsFilter.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/RunAsFilter.java
index 0ee720a..9940cd9 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/RunAsFilter.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/RunAsFilter.java
@@ -21,7 +21,6 @@
import com.google.gerrit.extensions.registration.DynamicItem;
import com.google.gerrit.extensions.restapi.AuthException;
import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.account.AccountResolver;
import com.google.gerrit.server.config.AuthConfig;
@@ -30,7 +29,6 @@
import com.google.gerrit.server.permissions.PermissionBackendException;
import com.google.gwtorm.server.OrmException;
import com.google.inject.Inject;
-import com.google.inject.Provider;
import com.google.inject.Singleton;
import com.google.inject.servlet.ServletModule;
import java.io.IOException;
@@ -59,7 +57,6 @@
}
}
- private final Provider<ReviewDb> db;
private final boolean enabled;
private final DynamicItem<WebSession> session;
private final PermissionBackend permissionBackend;
@@ -67,12 +64,10 @@
@Inject
RunAsFilter(
- Provider<ReviewDb> db,
AuthConfig config,
DynamicItem<WebSession> session,
PermissionBackend permissionBackend,
AccountResolver accountResolver) {
- this.db = db;
this.enabled = config.isRunAsEnabled();
this.session = session;
this.permissionBackend = permissionBackend;
@@ -111,7 +106,7 @@
Account target;
try {
- target = accountResolver.find(db.get(), runas);
+ target = accountResolver.find(runas);
} catch (OrmException | IOException | ConfigInvalidException e) {
log.warn("cannot resolve account for " + RUN_AS, e);
replyError(req, res, SC_INTERNAL_SERVER_ERROR, "cannot resolve " + RUN_AS, e);
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/PolyGerritUiServlet.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/PolyGerritUiServlet.java
index 2f3d32f..c508b2d 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/PolyGerritUiServlet.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/PolyGerritUiServlet.java
@@ -15,11 +15,17 @@
package com.google.gerrit.httpd.raw;
import com.google.common.cache.Cache;
+import com.google.gerrit.common.TimeUtil;
+import java.io.IOException;
+import java.nio.file.FileSystems;
import java.nio.file.Path;
+import java.nio.file.attribute.FileTime;
class PolyGerritUiServlet extends ResourceServlet {
private static final long serialVersionUID = 1L;
+ private static final FileTime NOW = FileTime.fromMillis(TimeUtil.nowMs());
+
private final Path ui;
PolyGerritUiServlet(Cache<Path, Resource> cache, Path ui) {
@@ -31,4 +37,16 @@
protected Path getResourcePath(String pathInfo) {
return ui.resolve(pathInfo);
}
+
+ @Override
+ protected FileTime getLastModifiedTime(Path p) throws IOException {
+ if (ui.getFileSystem().equals(FileSystems.getDefault())) {
+ // Assets are being served from disk, so we can trust the mtime.
+ return super.getLastModifiedTime(p);
+ }
+ // Assume this FileSystem is serving from a WAR. All WAR outputs from the build process have
+ // mtimes of 1980/1/1, so we can't trust it, and return the initialization time of this class
+ // instead.
+ return NOW;
+ }
}
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/ResourceServlet.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/ResourceServlet.java
index 150acc6..94ee221 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/ResourceServlet.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/ResourceServlet.java
@@ -31,7 +31,6 @@
import com.google.common.cache.Cache;
import com.google.common.collect.ImmutableMap;
import com.google.common.hash.Hashing;
-import com.google.gerrit.common.FileUtil;
import com.google.gerrit.common.Nullable;
import com.google.gerrit.httpd.HtmlDomUtil;
import com.google.gwtexpui.server.CacheHeaders;
@@ -252,7 +251,7 @@
return true;
}
- long lastModified = FileUtil.lastModified(p);
+ long lastModified = getLastModifiedTime(p).toMillis();
if (req.getDateHeader(IF_MODIFIED_SINCE) >= lastModified) {
rsp.setStatus(SC_NOT_MODIFIED);
return true;
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/WarDocServlet.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/WarDocServlet.java
index 93bd5ae..3f6ff25 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/WarDocServlet.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/WarDocServlet.java
@@ -15,12 +15,16 @@
package com.google.gerrit.httpd.raw;
import com.google.common.cache.Cache;
+import com.google.gerrit.common.TimeUtil;
import java.nio.file.FileSystem;
import java.nio.file.Path;
+import java.nio.file.attribute.FileTime;
class WarDocServlet extends ResourceServlet {
private static final long serialVersionUID = 1L;
+ private static final FileTime NOW = FileTime.fromMillis(TimeUtil.nowMs());
+
private final FileSystem warFs;
WarDocServlet(Cache<Path, Resource> cache, FileSystem warFs) {
@@ -32,4 +36,11 @@
protected Path getResourcePath(String pathInfo) {
return warFs.getPath("/Documentation/" + pathInfo);
}
+
+ @Override
+ protected FileTime getLastModifiedTime(Path p) {
+ // Return initialization time of this class, since the WAR outputs from the build process all
+ // have mtimes of 1980/1/1.
+ return NOW;
+ }
}
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/Daemon.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/Daemon.java
index a71a7fae..6b5c157 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/Daemon.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/Daemon.java
@@ -51,6 +51,7 @@
import com.google.gerrit.pgm.util.SiteProgram;
import com.google.gerrit.server.LibModuleLoader;
import com.google.gerrit.server.StartupChecks;
+import com.google.gerrit.server.account.AccountDeactivator;
import com.google.gerrit.server.account.InternalAccountDirectory;
import com.google.gerrit.server.cache.h2.DefaultCacheFactory;
import com.google.gerrit.server.change.ChangeCleanupRunner;
@@ -461,6 +462,7 @@
});
modules.add(new GarbageCollectionModule());
if (!slave) {
+ modules.add(new AccountDeactivator.Module());
modules.add(new ChangeCleanupRunner.Module());
}
modules.addAll(LibModuleLoader.loadModules(cfgInjector));
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/StarredChangesUtil.java b/gerrit-server/src/main/java/com/google/gerrit/server/StarredChangesUtil.java
index 13c24e0..a8cc0f4 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/StarredChangesUtil.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/StarredChangesUtil.java
@@ -17,6 +17,7 @@
import static com.google.common.base.Preconditions.checkNotNull;
import static java.nio.charset.StandardCharsets.UTF_8;
import static java.util.stream.Collectors.joining;
+import static java.util.stream.Collectors.toSet;
import com.google.auto.value.AutoValue;
import com.google.common.base.CharMatcher;
@@ -26,6 +27,7 @@
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.ImmutableSortedSet;
+import com.google.common.collect.Sets;
import com.google.common.primitives.Ints;
import com.google.gerrit.common.Nullable;
import com.google.gerrit.reviewdb.client.Account;
@@ -49,6 +51,7 @@
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
+import java.util.Optional;
import java.util.Set;
import java.util.SortedSet;
import java.util.TreeSet;
@@ -154,7 +157,8 @@
public static final String DEFAULT_LABEL = "star";
public static final String IGNORE_LABEL = "ignore";
- public static final String MUTE_LABEL = "mute";
+ public static final String REVIEWED_LABEL = "reviewed";
+ public static final String UNREVIEWED_LABEL = "unreviewed";
public static final ImmutableSortedSet<String> DEFAULT_LABELS =
ImmutableSortedSet.of(DEFAULT_LABEL);
@@ -330,37 +334,41 @@
return isIgnoredBy(rsrc.getChange().getId(), rsrc.getUser().asIdentifiedUser().getAccountId());
}
- private static String getMuteLabel(Change change) {
- return MUTE_LABEL + "/" + change.currentPatchSetId().get();
+ private static String getReviewedLabel(Change change) {
+ return getReviewedLabel(change.currentPatchSetId().get());
}
- public void mute(ChangeResource rsrc) throws OrmException, IllegalLabelException {
+ private static String getReviewedLabel(int ps) {
+ return REVIEWED_LABEL + "/" + ps;
+ }
+
+ private static String getUnreviewedLabel(Change change) {
+ return getUnreviewedLabel(change.currentPatchSetId().get());
+ }
+
+ private static String getUnreviewedLabel(int ps) {
+ return UNREVIEWED_LABEL + "/" + ps;
+ }
+
+ public void markAsReviewed(ChangeResource rsrc) throws OrmException, IllegalLabelException {
star(
rsrc.getUser().asIdentifiedUser().getAccountId(),
rsrc.getProject(),
rsrc.getChange().getId(),
- ImmutableSet.of(getMuteLabel(rsrc.getChange())),
- ImmutableSet.of());
+ ImmutableSet.of(getReviewedLabel(rsrc.getChange())),
+ ImmutableSet.of(getUnreviewedLabel(rsrc.getChange())));
}
- public void unmute(ChangeResource rsrc) throws OrmException, IllegalLabelException {
+ public void markAsUnreviewed(ChangeResource rsrc) throws OrmException, IllegalLabelException {
star(
rsrc.getUser().asIdentifiedUser().getAccountId(),
rsrc.getProject(),
rsrc.getChange().getId(),
- ImmutableSet.of(),
- ImmutableSet.of(getMuteLabel(rsrc.getChange())));
+ ImmutableSet.of(getUnreviewedLabel(rsrc.getChange())),
+ ImmutableSet.of(getReviewedLabel(rsrc.getChange())));
}
- public boolean isMutedBy(Change change, Account.Id accountId) throws OrmException {
- return getLabels(accountId, change.getId()).contains(getMuteLabel(change));
- }
-
- public boolean isMuted(ChangeResource rsrc) throws OrmException {
- return isMutedBy(rsrc.getChange(), rsrc.getUser().asIdentifiedUser().getAccountId());
- }
-
- private static StarRef readLabels(Repository repo, String refName) throws IOException {
+ public static StarRef readLabels(Repository repo, String refName) throws IOException {
Ref ref = repo.exactRef(refName);
if (ref == null) {
return StarRef.MISSING;
@@ -394,6 +402,25 @@
if (labels.containsAll(ImmutableSet.of(DEFAULT_LABEL, IGNORE_LABEL))) {
throw new MutuallyExclusiveLabelsException(DEFAULT_LABEL, IGNORE_LABEL);
}
+
+ Set<Integer> reviewedPatchSets =
+ labels
+ .stream()
+ .filter(l -> l.startsWith(REVIEWED_LABEL))
+ .map(l -> Integer.valueOf(l.substring(REVIEWED_LABEL.length() + 1)))
+ .collect(toSet());
+ Set<Integer> unreviewedPatchSets =
+ labels
+ .stream()
+ .filter(l -> l.startsWith(UNREVIEWED_LABEL))
+ .map(l -> Integer.valueOf(l.substring(UNREVIEWED_LABEL.length() + 1)))
+ .collect(toSet());
+ Optional<Integer> ps =
+ Sets.intersection(reviewedPatchSets, unreviewedPatchSets).stream().findFirst();
+ if (ps.isPresent()) {
+ throw new MutuallyExclusiveLabelsException(
+ getReviewedLabel(ps.get()), getUnreviewedLabel(ps.get()));
+ }
}
private static void validateLabels(Collection<String> labels) throws InvalidLabelsException {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountDeactivator.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountDeactivator.java
new file mode 100644
index 0000000..c222756
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountDeactivator.java
@@ -0,0 +1,121 @@
+// Copyright (C) 2017 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.account;
+
+import static com.google.gerrit.server.config.ScheduleConfig.MISSING_CONFIG;
+
+import com.google.gerrit.extensions.events.LifecycleListener;
+import com.google.gerrit.lifecycle.LifecycleModule;
+import com.google.gerrit.server.config.GerritServerConfig;
+import com.google.gerrit.server.config.ScheduleConfig;
+import com.google.gerrit.server.git.WorkQueue;
+import com.google.gerrit.server.query.account.AccountPredicates;
+import com.google.gerrit.server.query.account.InternalAccountQuery;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import java.util.concurrent.TimeUnit;
+import org.eclipse.jgit.lib.Config;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/** Runnable to enable scheduling account deactivations to run periodically */
+public class AccountDeactivator implements Runnable {
+ private static final Logger log = LoggerFactory.getLogger(AccountDeactivator.class);
+
+ public static class Module extends LifecycleModule {
+ @Override
+ protected void configure() {
+ listener().to(Lifecycle.class);
+ }
+ }
+
+ static class Lifecycle implements LifecycleListener {
+ private final WorkQueue queue;
+ private final AccountDeactivator deactivator;
+ private final boolean supportAutomaticAccountActivityUpdate;
+ private final ScheduleConfig scheduleConfig;
+
+ @Inject
+ Lifecycle(WorkQueue queue, AccountDeactivator deactivator, @GerritServerConfig Config cfg) {
+ this.queue = queue;
+ this.deactivator = deactivator;
+ scheduleConfig = new ScheduleConfig(cfg, "accountDeactivation");
+ supportAutomaticAccountActivityUpdate =
+ cfg.getBoolean("auth", "autoUpdateAccountActiveStatus", false);
+ }
+
+ @Override
+ public void start() {
+ if (!supportAutomaticAccountActivityUpdate) {
+ return;
+ }
+ long interval = scheduleConfig.getInterval();
+ long delay = scheduleConfig.getInitialDelay();
+ if (delay == MISSING_CONFIG && interval == MISSING_CONFIG) {
+ log.info("Ignoring missing accountDeactivator schedule configuration");
+ } else if (delay < 0 || interval <= 0) {
+ log.warn(
+ String.format(
+ "Ignoring invalid accountDeactivator schedule configuration: %s", scheduleConfig));
+ } else {
+ queue
+ .getDefaultQueue()
+ .scheduleAtFixedRate(deactivator, delay, interval, TimeUnit.MILLISECONDS);
+ }
+ }
+
+ @Override
+ public void stop() {
+ // handled by WorkQueue.stop() already
+ }
+ }
+
+ private final Provider<InternalAccountQuery> accountQueryProvider;
+ private final Realm realm;
+ private final SetInactiveFlag sif;
+
+ @Inject
+ AccountDeactivator(
+ Provider<InternalAccountQuery> accountQueryProvider, SetInactiveFlag sif, Realm realm) {
+ this.accountQueryProvider = accountQueryProvider;
+ this.sif = sif;
+ this.realm = realm;
+ }
+
+ @Override
+ public void run() {
+ log.debug("Running account deactivations");
+ try {
+ int numberOfAccountsDeactivated = 0;
+ for (AccountState acc : accountQueryProvider.get().query(AccountPredicates.isActive())) {
+ log.debug("processing account " + acc.getUserName());
+ if (acc.getUserName() != null && !realm.isActive(acc.getUserName())) {
+ sif.deactivate(acc.getAccount().getId());
+ log.debug("deactivated accout " + acc.getUserName());
+ numberOfAccountsDeactivated++;
+ }
+ }
+ log.info(
+ "Deactivations complete, {} account(s) were deactivated", numberOfAccountsDeactivated);
+ } catch (Exception e) {
+ log.error("Failed to deactivate inactive accounts " + e.getMessage(), e);
+ }
+ }
+
+ @Override
+ public String toString() {
+ return "account deactivator";
+ }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountManager.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountManager.java
index ec756bc..046b60a 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountManager.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountManager.java
@@ -22,6 +22,8 @@
import com.google.gerrit.common.errors.NameAlreadyUsedException;
import com.google.gerrit.common.errors.NoSuchGroupException;
import com.google.gerrit.extensions.client.AccountFieldName;
+import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
+import com.google.gerrit.extensions.restapi.RestApiException;
import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.reviewdb.client.AccountGroup;
import com.google.gerrit.reviewdb.server.ReviewDb;
@@ -70,6 +72,8 @@
private final ExternalIds externalIds;
private final ExternalIdsUpdate.Server externalIdsUpdateFactory;
private final GroupsUpdate.Factory groupsUpdateFactory;
+ private final boolean autoUpdateAccountActiveStatus;
+ private final SetInactiveFlag setInactiveFlag;
@Inject
AccountManager(
@@ -86,7 +90,8 @@
Provider<InternalAccountQuery> accountQueryProvider,
ExternalIds externalIds,
ExternalIdsUpdate.Server externalIdsUpdateFactory,
- GroupsUpdate.Factory groupsUpdateFactory) {
+ GroupsUpdate.Factory groupsUpdateFactory,
+ SetInactiveFlag setInactiveFlag) {
this.schema = schema;
this.sequences = sequences;
this.accounts = accounts;
@@ -102,6 +107,9 @@
this.externalIds = externalIds;
this.externalIdsUpdateFactory = externalIdsUpdateFactory;
this.groupsUpdateFactory = groupsUpdateFactory;
+ this.autoUpdateAccountActiveStatus =
+ cfg.getBoolean("auth", "autoUpdateAccountActiveStatus", false);
+ this.setInactiveFlag = setInactiveFlag;
}
/** @return user identified by this external identity string */
@@ -122,8 +130,8 @@
* @param who identity of the user, with any details we received about them.
* @return the result of authenticating the user.
* @throws AccountException the account does not exist, and cannot be created, or exists, but
- * cannot be located, or is inactive, or cannot be added to the admin group (only for the
- * first account).
+ * cannot be located, is unable to be activated or deactivated, or is inactive, or cannot be
+ * added to the admin group (only for the first account).
*/
public AuthResult authenticate(AuthRequest who) throws AccountException, IOException {
who = realm.authenticate(who);
@@ -138,6 +146,24 @@
// Account exists
Account act = byIdCache.get(id.accountId()).getAccount();
+ if (autoUpdateAccountActiveStatus && who.authProvidesAccountActiveStatus()) {
+ if (who.isActive() && !act.isActive()) {
+ try {
+ setInactiveFlag.activate(act.getId());
+ act = byIdCache.get(id.accountId()).getAccount();
+ } catch (ResourceNotFoundException e) {
+ throw new AccountException("Unable to activate account " + act.getId(), e);
+ }
+ } else if (!who.isActive() && act.isActive()) {
+ try {
+ setInactiveFlag.deactivate(act.getId());
+ act = byIdCache.get(id.accountId()).getAccount();
+ } catch (RestApiException e) {
+ throw new AccountException("Unable to deactivate account " + act.getId(), e);
+ }
+ }
+ }
+
if (!act.isActive()) {
throw new AccountException("Authentication error, account inactive");
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountResolver.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountResolver.java
index 94f63a7..a1cca06 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountResolver.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountResolver.java
@@ -17,7 +17,6 @@
import static java.util.stream.Collectors.toSet;
import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gerrit.server.query.account.InternalAccountQuery;
import com.google.gwtorm.server.OrmException;
import com.google.inject.Inject;
@@ -63,9 +62,8 @@
* @return the single account that matches; null if no account matches or there are multiple
* candidates.
*/
- public Account find(ReviewDb db, String nameOrEmail)
- throws OrmException, IOException, ConfigInvalidException {
- Set<Account.Id> r = findAll(db, nameOrEmail);
+ public Account find(String nameOrEmail) throws OrmException, IOException, ConfigInvalidException {
+ Set<Account.Id> r = findAll(nameOrEmail);
if (r.size() == 1) {
return byId.get(r.iterator().next()).getAccount();
}
@@ -87,13 +85,12 @@
/**
* Find all accounts matching the name or name/email string.
*
- * @param db open database handle.
* @param nameOrEmail a string of the format "Full Name <email@example>", just the email
* address ("email@example"), a full name ("Full Name"), an account id ("18419") or an user
* name ("username").
* @return the accounts that match, empty collection if none. Never null.
*/
- public Set<Account.Id> findAll(ReviewDb db, String nameOrEmail)
+ public Set<Account.Id> findAll(String nameOrEmail)
throws OrmException, IOException, ConfigInvalidException {
Matcher m = Pattern.compile("^.* \\(([1-9][0-9]*)\\)$").matcher(nameOrEmail);
if (m.matches()) {
@@ -119,34 +116,30 @@
}
}
- return findAllByNameOrEmail(db, nameOrEmail);
+ return findAllByNameOrEmail(nameOrEmail);
}
/**
* Locate exactly one account matching the name or name/email string.
*
- * @param db open database handle.
* @param nameOrEmail a string of the format "Full Name <email@example>", just the email
* address ("email@example"), a full name ("Full Name").
* @return the single account that matches; null if no account matches or there are multiple
* candidates.
*/
- public Account findByNameOrEmail(ReviewDb db, String nameOrEmail)
- throws OrmException, IOException {
- Set<Account.Id> r = findAllByNameOrEmail(db, nameOrEmail);
+ public Account findByNameOrEmail(String nameOrEmail) throws OrmException, IOException {
+ Set<Account.Id> r = findAllByNameOrEmail(nameOrEmail);
return r.size() == 1 ? byId.get(r.iterator().next()).getAccount() : null;
}
/**
* Locate exactly one account matching the name or name/email string.
*
- * @param db open database handle.
* @param nameOrEmail a string of the format "Full Name <email@example>", just the email
* address ("email@example"), a full name ("Full Name").
* @return the accounts that match, empty collection if none. Never null.
*/
- public Set<Account.Id> findAllByNameOrEmail(ReviewDb db, String nameOrEmail)
- throws OrmException, IOException {
+ public Set<Account.Id> findAllByNameOrEmail(String nameOrEmail) throws OrmException, IOException {
int lt = nameOrEmail.indexOf('<');
int gt = nameOrEmail.indexOf('>');
if (lt >= 0 && gt > lt && nameOrEmail.contains("@")) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountsCollection.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountsCollection.java
index dcda816..19a8259 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountsCollection.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountsCollection.java
@@ -25,7 +25,6 @@
import com.google.gerrit.extensions.restapi.TopLevelResource;
import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gerrit.server.AnonymousUser;
import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.IdentifiedUser;
@@ -39,7 +38,6 @@
@Singleton
public class AccountsCollection
implements RestCollection<TopLevelResource, AccountResource>, AcceptsCreate<TopLevelResource> {
- private final Provider<ReviewDb> db;
private final Provider<CurrentUser> self;
private final AccountResolver resolver;
private final AccountControl.Factory accountControlFactory;
@@ -50,7 +48,6 @@
@Inject
AccountsCollection(
- Provider<ReviewDb> db,
Provider<CurrentUser> self,
AccountResolver resolver,
AccountControl.Factory accountControlFactory,
@@ -58,7 +55,6 @@
Provider<QueryAccounts> list,
DynamicMap<RestView<AccountResource>> views,
CreateAccount.Factory createAccountFactory) {
- this.db = db;
this.self = self;
this.resolver = resolver;
this.accountControlFactory = accountControlFactory;
@@ -144,7 +140,7 @@
}
}
- Account match = resolver.find(db.get(), id);
+ Account match = resolver.find(id);
if (match == null) {
return null;
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/AuthRequest.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/AuthRequest.java
index e654b8d..6647ca4 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/AuthRequest.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/AuthRequest.java
@@ -63,6 +63,8 @@
private boolean skipAuthentication;
private String authPlugin;
private String authProvider;
+ private boolean authProvidesAccountActiveStatus;
+ private boolean active;
public AuthRequest(ExternalId.Key externalId) {
this.externalId = externalId;
@@ -140,4 +142,20 @@
public void setAuthProvider(String authProvider) {
this.authProvider = authProvider;
}
+
+ public boolean authProvidesAccountActiveStatus() {
+ return authProvidesAccountActiveStatus;
+ }
+
+ public void setAuthProvidesAccountActiveStatus(boolean authProvidesAccountActiveStatus) {
+ this.authProvidesAccountActiveStatus = authProvidesAccountActiveStatus;
+ }
+
+ public boolean isActive() {
+ return active;
+ }
+
+ public void setActive(Boolean isActive) {
+ this.active = isActive;
+ }
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/DeleteActive.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/DeleteActive.java
index 0ff5342..43669c0 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/DeleteActive.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/DeleteActive.java
@@ -49,6 +49,6 @@
if (self.get() == rsrc.getUser()) {
throw new ResourceConflictException("cannot deactivate own account");
}
- return setInactiveFlag.deactivate(rsrc.getUser());
+ return setInactiveFlag.deactivate(rsrc.getUser().getAccountId());
}
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/PutActive.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/PutActive.java
index 825ef10..7ce2ea8 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/PutActive.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/PutActive.java
@@ -41,6 +41,6 @@
@Override
public Response<String> apply(AccountResource rsrc, Input input)
throws ResourceNotFoundException, OrmException, IOException, ConfigInvalidException {
- return setInactiveFlag.activate(rsrc.getUser());
+ return setInactiveFlag.activate(rsrc.getUser().getAccountId());
}
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/Realm.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/Realm.java
index b5e4cba..c375dd6 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/Realm.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/Realm.java
@@ -19,6 +19,8 @@
import com.google.gerrit.server.IdentifiedUser;
import java.io.IOException;
import java.util.Set;
+import javax.naming.NamingException;
+import javax.security.auth.login.LoginException;
public interface Realm {
/** Can the end-user modify this field of their own account? */
@@ -45,4 +47,15 @@
* into an email address, and then locate the user by that email address.
*/
Account.Id lookup(String accountName) throws IOException;
+
+ /**
+ * @return true if the account is active.
+ * @throws NamingException
+ * @throws LoginException
+ * @throws AccountException
+ */
+ default boolean isActive(@SuppressWarnings("unused") String username)
+ throws LoginException, NamingException, AccountException {
+ return true;
+ }
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/SetInactiveFlag.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/SetInactiveFlag.java
index 1698387..6e12c3e 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/SetInactiveFlag.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/SetInactiveFlag.java
@@ -19,7 +19,6 @@
import com.google.gerrit.extensions.restapi.Response;
import com.google.gerrit.extensions.restapi.RestApiException;
import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.server.IdentifiedUser;
import com.google.inject.Inject;
import com.google.inject.Singleton;
import java.io.IOException;
@@ -36,14 +35,14 @@
this.accountsUpdate = accountsUpdate;
}
- public Response<?> deactivate(IdentifiedUser user)
+ public Response<?> deactivate(Account.Id accountId)
throws RestApiException, IOException, ConfigInvalidException {
AtomicBoolean alreadyInactive = new AtomicBoolean(false);
Account account =
accountsUpdate
.create()
.update(
- user.getAccountId(),
+ accountId,
a -> {
if (!a.isActive()) {
alreadyInactive.set(true);
@@ -60,14 +59,14 @@
return Response.none();
}
- public Response<String> activate(IdentifiedUser user)
+ public Response<String> activate(Account.Id accountId)
throws ResourceNotFoundException, IOException, ConfigInvalidException {
AtomicBoolean alreadyActive = new AtomicBoolean(false);
Account account =
accountsUpdate
.create()
.update(
- user.getAccountId(),
+ accountId,
a -> {
if (a.isActive()) {
alreadyActive.set(true);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/ChangeApiImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/ChangeApiImpl.java
index d43327f..0fba74a 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/ChangeApiImpl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/ChangeApiImpl.java
@@ -69,8 +69,9 @@
import com.google.gerrit.server.change.ListChangeComments;
import com.google.gerrit.server.change.ListChangeDrafts;
import com.google.gerrit.server.change.ListChangeRobotComments;
+import com.google.gerrit.server.change.MarkAsReviewed;
+import com.google.gerrit.server.change.MarkAsUnreviewed;
import com.google.gerrit.server.change.Move;
-import com.google.gerrit.server.change.Mute;
import com.google.gerrit.server.change.PostHashtags;
import com.google.gerrit.server.change.PostPrivate;
import com.google.gerrit.server.change.PostReviewers;
@@ -88,7 +89,6 @@
import com.google.gerrit.server.change.SubmittedTogether;
import com.google.gerrit.server.change.SuggestChangeReviewers;
import com.google.gerrit.server.change.Unignore;
-import com.google.gerrit.server.change.Unmute;
import com.google.gerrit.server.change.WorkInProgressOp;
import com.google.gwtorm.server.OrmException;
import com.google.inject.Inject;
@@ -140,8 +140,8 @@
private final DeletePrivate deletePrivate;
private final Ignore ignore;
private final Unignore unignore;
- private final Mute mute;
- private final Unmute unmute;
+ private final MarkAsReviewed markAsReviewed;
+ private final MarkAsUnreviewed markAsUnreviewed;
private final SetWorkInProgress setWip;
private final SetReadyForReview setReady;
private final PutMessage putMessage;
@@ -185,8 +185,8 @@
DeletePrivate deletePrivate,
Ignore ignore,
Unignore unignore,
- Mute mute,
- Unmute unmute,
+ MarkAsReviewed markAsReviewed,
+ MarkAsUnreviewed markAsUnreviewed,
SetWorkInProgress setWip,
SetReadyForReview setReady,
PutMessage putMessage,
@@ -228,8 +228,8 @@
this.deletePrivate = deletePrivate;
this.ignore = ignore;
this.unignore = unignore;
- this.mute = mute;
- this.unmute = unmute;
+ this.markAsReviewed = markAsReviewed;
+ this.markAsUnreviewed = markAsUnreviewed;
this.setWip = setWip;
this.setReady = setReady;
this.putMessage = putMessage;
@@ -677,26 +677,18 @@
}
@Override
- public void mute(boolean mute) throws RestApiException {
+ public void markAsReviewed(boolean reviewed) throws RestApiException {
// TODO(dborowitz): Convert to RetryingRestModifyView. Needs to plumb BatchUpdate.Factory into
// StarredChangesUtil.
try {
- if (mute) {
- this.mute.apply(change, new Mute.Input());
+ if (reviewed) {
+ markAsReviewed.apply(change, new MarkAsReviewed.Input());
} else {
- unmute.apply(change, new Unmute.Input());
+ markAsUnreviewed.apply(change, new MarkAsUnreviewed.Input());
}
} catch (OrmException | IllegalLabelException e) {
- throw asRestApiException("Cannot mute change", e);
- }
- }
-
- @Override
- public boolean muted() throws RestApiException {
- try {
- return stars.isMuted(change);
- } catch (OrmException e) {
- throw asRestApiException("Cannot check if muted", e);
+ throw asRestApiException(
+ "Cannot mark change as " + (reviewed ? "reviewed" : "unreviewed"), e);
}
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/api/projects/DashboardApiImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/api/projects/DashboardApiImpl.java
index 58cb59e..f0a6009 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/api/projects/DashboardApiImpl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/api/projects/DashboardApiImpl.java
@@ -18,6 +18,7 @@
import com.google.gerrit.extensions.api.projects.DashboardApi;
import com.google.gerrit.extensions.api.projects.DashboardInfo;
+import com.google.gerrit.extensions.common.SetDashboardInput;
import com.google.gerrit.extensions.restapi.IdString;
import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
import com.google.gerrit.extensions.restapi.RestApiException;
@@ -26,6 +27,7 @@
import com.google.gerrit.server.project.DashboardsCollection;
import com.google.gerrit.server.project.GetDashboard;
import com.google.gerrit.server.project.ProjectResource;
+import com.google.gerrit.server.project.SetDashboard;
import com.google.inject.Inject;
import com.google.inject.Provider;
import com.google.inject.assistedinject.Assisted;
@@ -39,6 +41,7 @@
private final DashboardsCollection dashboards;
private final Provider<GetDashboard> get;
+ private final SetDashboard set;
private final ProjectResource project;
private final String id;
@@ -46,10 +49,12 @@
DashboardApiImpl(
DashboardsCollection dashboards,
Provider<GetDashboard> get,
+ SetDashboard set,
@Assisted ProjectResource project,
@Assisted String id) {
this.dashboards = dashboards;
this.get = get;
+ this.set = set;
this.project = project;
this.id = id;
}
@@ -68,6 +73,17 @@
}
}
+ @Override
+ public void setDefault() throws RestApiException {
+ SetDashboardInput input = new SetDashboardInput();
+ input.id = id;
+ try {
+ set.apply(DashboardResource.projectDefault(project.getControl()), input);
+ } catch (Exception e) {
+ throw asRestApiException("Cannot set default dashboard", e);
+ }
+ }
+
private DashboardResource resource()
throws ResourceNotFoundException, IOException, ConfigInvalidException,
PermissionBackendException {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/api/projects/ProjectApiImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/api/projects/ProjectApiImpl.java
index 5012280..89c92d0 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/api/projects/ProjectApiImpl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/api/projects/ProjectApiImpl.java
@@ -16,6 +16,7 @@
import static com.google.gerrit.server.api.ApiUtil.asRestApiException;
import static com.google.gerrit.server.project.DashboardsCollection.DEFAULT_DASHBOARD_NAME;
+import static java.util.stream.Collectors.toList;
import com.google.gerrit.extensions.api.access.ProjectAccessInfo;
import com.google.gerrit.extensions.api.access.ProjectAccessInput;
@@ -28,6 +29,7 @@
import com.google.gerrit.extensions.api.projects.ConfigInfo;
import com.google.gerrit.extensions.api.projects.ConfigInput;
import com.google.gerrit.extensions.api.projects.DashboardApi;
+import com.google.gerrit.extensions.api.projects.DashboardInfo;
import com.google.gerrit.extensions.api.projects.DeleteBranchesInput;
import com.google.gerrit.extensions.api.projects.DeleteTagsInput;
import com.google.gerrit.extensions.api.projects.DescriptionInput;
@@ -39,6 +41,7 @@
import com.google.gerrit.extensions.common.ProjectInfo;
import com.google.gerrit.extensions.restapi.BadRequestException;
import com.google.gerrit.extensions.restapi.IdString;
+import com.google.gerrit.extensions.restapi.NotImplementedException;
import com.google.gerrit.extensions.restapi.ResourceConflictException;
import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
import com.google.gerrit.extensions.restapi.RestApiException;
@@ -58,6 +61,7 @@
import com.google.gerrit.server.project.GetDescription;
import com.google.gerrit.server.project.ListBranches;
import com.google.gerrit.server.project.ListChildProjects;
+import com.google.gerrit.server.project.ListDashboards;
import com.google.gerrit.server.project.ListTags;
import com.google.gerrit.server.project.ProjectJson;
import com.google.gerrit.server.project.ProjectResource;
@@ -68,6 +72,7 @@
import com.google.inject.Provider;
import com.google.inject.assistedinject.Assisted;
import com.google.inject.assistedinject.AssistedInject;
+import java.util.Collections;
import java.util.List;
public class ProjectApiImpl implements ProjectApi {
@@ -104,6 +109,7 @@
private final CommitApiImpl.Factory commitApi;
private final DashboardApiImpl.Factory dashboardApi;
private final CheckAccess checkAccess;
+ private final Provider<ListDashboards> listDashboards;
@AssistedInject
ProjectApiImpl(
@@ -132,6 +138,7 @@
CommitApiImpl.Factory commitApi,
DashboardApiImpl.Factory dashboardApi,
CheckAccess checkAccess,
+ Provider<ListDashboards> listDashboards,
@Assisted ProjectResource project) {
this(
user,
@@ -160,6 +167,7 @@
commitApi,
dashboardApi,
checkAccess,
+ listDashboards,
null);
}
@@ -190,6 +198,7 @@
CommitApiImpl.Factory commitApi,
DashboardApiImpl.Factory dashboardApi,
CheckAccess checkAccess,
+ Provider<ListDashboards> listDashboards,
@Assisted String name) {
this(
user,
@@ -218,6 +227,7 @@
commitApi,
dashboardApi,
checkAccess,
+ listDashboards,
name);
}
@@ -248,6 +258,7 @@
CommitApiImpl.Factory commitApi,
DashboardApiImpl.Factory dashboardApi,
CheckAccess checkAccess,
+ Provider<ListDashboards> listDashboards,
String name) {
this.user = user;
this.permissionBackend = permissionBackend;
@@ -275,6 +286,7 @@
this.createAccessChange = createAccessChange;
this.dashboardApi = dashboardApi;
this.checkAccess = checkAccess;
+ this.listDashboards = listDashboards;
this.name = name;
}
@@ -473,6 +485,27 @@
return dashboard(DEFAULT_DASHBOARD_NAME);
}
+ @Override
+ public ListDashboardsRequest dashboards() throws RestApiException {
+ return new ListDashboardsRequest() {
+ @Override
+ public List<DashboardInfo> get() throws RestApiException {
+ try {
+ List<?> r = listDashboards.get().apply(checkExists());
+ if (r.isEmpty()) {
+ return Collections.emptyList();
+ }
+ if (r.get(0) instanceof DashboardInfo) {
+ return r.stream().map(i -> (DashboardInfo) i).collect(toList());
+ }
+ throw new NotImplementedException("list with inheritance");
+ } catch (Exception e) {
+ throw asRestApiException("Cannot list dashboards", e);
+ }
+ }
+ };
+ }
+
private ProjectResource checkExists() throws ResourceNotFoundException {
if (project == null) {
throw new ResourceNotFoundException(name);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/args4j/AccountIdHandler.java b/gerrit-server/src/main/java/com/google/gerrit/server/args4j/AccountIdHandler.java
index ce31cac..988b9df7 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/args4j/AccountIdHandler.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/args4j/AccountIdHandler.java
@@ -16,7 +16,6 @@
import com.google.gerrit.extensions.client.AuthType;
import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gerrit.server.account.AccountException;
import com.google.gerrit.server.account.AccountManager;
import com.google.gerrit.server.account.AccountResolver;
@@ -24,7 +23,6 @@
import com.google.gerrit.server.config.AuthConfig;
import com.google.gwtorm.server.OrmException;
import com.google.inject.Inject;
-import com.google.inject.Provider;
import com.google.inject.assistedinject.Assisted;
import java.io.IOException;
import org.eclipse.jgit.errors.ConfigInvalidException;
@@ -36,14 +34,12 @@
import org.kohsuke.args4j.spi.Setter;
public class AccountIdHandler extends OptionHandler<Account.Id> {
- private final Provider<ReviewDb> db;
private final AccountResolver accountResolver;
private final AccountManager accountManager;
private final AuthType authType;
@Inject
public AccountIdHandler(
- Provider<ReviewDb> db,
AccountResolver accountResolver,
AccountManager accountManager,
AuthConfig authConfig,
@@ -51,7 +47,6 @@
@Assisted OptionDef option,
@Assisted Setter<Account.Id> setter) {
super(parser, option, setter);
- this.db = db;
this.accountResolver = accountResolver;
this.accountManager = accountManager;
this.authType = authConfig.getAuthType();
@@ -62,7 +57,7 @@
String token = params.getParameter(0);
Account.Id accountId;
try {
- Account a = accountResolver.find(db.get(), token);
+ Account a = accountResolver.find(token);
if (a != null) {
accountId = a.getId();
} else {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/auth/ldap/LdapRealm.java b/gerrit-server/src/main/java/com/google/gerrit/server/auth/ldap/LdapRealm.java
index a34e3fc..ec803e5 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/auth/ldap/LdapRealm.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/auth/ldap/LdapRealm.java
@@ -33,6 +33,7 @@
import com.google.gerrit.server.account.externalids.ExternalId;
import com.google.gerrit.server.account.externalids.ExternalIds;
import com.google.gerrit.server.auth.AuthenticationUnavailableException;
+import com.google.gerrit.server.auth.NoSuchUserException;
import com.google.gerrit.server.config.AuthConfig;
import com.google.gerrit.server.config.GerritServerConfig;
import com.google.inject.Inject;
@@ -232,7 +233,15 @@
}
try {
final Helper.LdapSchema schema = helper.getSchema(ctx);
- final LdapQuery.Result m = helper.findAccount(schema, ctx, username, fetchMemberOfEagerly);
+ LdapQuery.Result m;
+ who.setAuthProvidesAccountActiveStatus(true);
+ try {
+ m = helper.findAccount(schema, ctx, username, fetchMemberOfEagerly);
+ who.setActive(true);
+ } catch (NoSuchUserException e) {
+ who.setActive(false);
+ return who;
+ }
if (authConfig.getAuthType() == AuthType.LDAP && !who.isSkipAuthentication()) {
// We found the user account, but we need to verify
@@ -314,6 +323,19 @@
}
}
+ @Override
+ public boolean isActive(String username)
+ throws LoginException, NamingException, AccountException {
+ try {
+ DirContext ctx = helper.open();
+ Helper.LdapSchema schema = helper.getSchema(ctx);
+ helper.findAccount(schema, ctx, username, false);
+ } catch (NoSuchUserException e) {
+ return false;
+ }
+ return true;
+ }
+
static class UserLoader extends CacheLoader<String, Optional<Account.Id>> {
private final ExternalIds externalIds;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeJson.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeJson.java
index e2237f3..8dc53bc 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeJson.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeJson.java
@@ -551,22 +551,13 @@
if (user.isIdentifiedUser()) {
Collection<String> stars = cd.stars(user.getAccountId());
out.starred = stars.contains(StarredChangesUtil.DEFAULT_LABEL) ? true : null;
- out.muted =
- stars.contains(StarredChangesUtil.MUTE_LABEL + "/" + cd.currentPatchSet().getPatchSetId())
- ? true
- : null;
if (!stars.isEmpty()) {
out.stars = stars;
}
}
if (in.getStatus().isOpen() && has(REVIEWED) && user.isIdentifiedUser()) {
- Account.Id accountId = user.getAccountId();
- if (out.muted != null) {
- out.reviewed = true;
- } else {
- out.reviewed = cd.reviewedBy().contains(accountId) ? true : null;
- }
+ out.reviewed = cd.isReviewedBy(user.getAccountId()) ? true : null;
}
out.labels = labelsFor(perm, cd, has(LABELS), has(DETAILED_LABELS));
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/MarkAsReviewed.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/MarkAsReviewed.java
new file mode 100644
index 0000000..265b2b0
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/MarkAsReviewed.java
@@ -0,0 +1,78 @@
+// Copyright (C) 2017 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.change;
+
+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.extensions.webui.UiAction;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.StarredChangesUtil;
+import com.google.gerrit.server.StarredChangesUtil.IllegalLabelException;
+import com.google.gerrit.server.query.change.ChangeData;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import com.google.inject.Singleton;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+@Singleton
+public class MarkAsReviewed
+ implements RestModifyView<ChangeResource, MarkAsReviewed.Input>, UiAction<ChangeResource> {
+ private static final Logger log = LoggerFactory.getLogger(MarkAsReviewed.class);
+
+ public static class Input {}
+
+ private final Provider<ReviewDb> dbProvider;
+ private final ChangeData.Factory changeDataFactory;
+ private final StarredChangesUtil stars;
+
+ @Inject
+ MarkAsReviewed(
+ Provider<ReviewDb> dbProvider,
+ ChangeData.Factory changeDataFactory,
+ StarredChangesUtil stars) {
+ this.dbProvider = dbProvider;
+ this.changeDataFactory = changeDataFactory;
+ this.stars = stars;
+ }
+
+ @Override
+ public Description getDescription(ChangeResource rsrc) {
+ return new UiAction.Description()
+ .setLabel("Mark Reviewed")
+ .setTitle("Mark the change as reviewed to unhighlight it in the dashboard")
+ .setVisible(!isReviewed(rsrc));
+ }
+
+ @Override
+ public Response<String> apply(ChangeResource rsrc, Input input)
+ throws RestApiException, OrmException, IllegalLabelException {
+ stars.markAsReviewed(rsrc);
+ return Response.ok("");
+ }
+
+ private boolean isReviewed(ChangeResource rsrc) {
+ try {
+ return changeDataFactory
+ .create(dbProvider.get(), rsrc.getNotes())
+ .isReviewedBy(rsrc.getUser().asIdentifiedUser().getAccountId());
+ } catch (OrmException e) {
+ log.error("failed to check if change is reviewed", e);
+ }
+ return false;
+ }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/MarkAsUnreviewed.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/MarkAsUnreviewed.java
new file mode 100644
index 0000000..6de84ee
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/MarkAsUnreviewed.java
@@ -0,0 +1,77 @@
+// Copyright (C) 2017 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.change;
+
+import com.google.gerrit.extensions.restapi.Response;
+import com.google.gerrit.extensions.restapi.RestModifyView;
+import com.google.gerrit.extensions.webui.UiAction;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.StarredChangesUtil;
+import com.google.gerrit.server.StarredChangesUtil.IllegalLabelException;
+import com.google.gerrit.server.query.change.ChangeData;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import com.google.inject.Singleton;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+@Singleton
+public class MarkAsUnreviewed
+ implements RestModifyView<ChangeResource, MarkAsUnreviewed.Input>, UiAction<ChangeResource> {
+ private static final Logger log = LoggerFactory.getLogger(MarkAsUnreviewed.class);
+
+ public static class Input {}
+
+ private final Provider<ReviewDb> dbProvider;
+ private final ChangeData.Factory changeDataFactory;
+ private final StarredChangesUtil stars;
+
+ @Inject
+ MarkAsUnreviewed(
+ Provider<ReviewDb> dbProvider,
+ ChangeData.Factory changeDataFactory,
+ StarredChangesUtil stars) {
+ this.dbProvider = dbProvider;
+ this.changeDataFactory = changeDataFactory;
+ this.stars = stars;
+ }
+
+ @Override
+ public Description getDescription(ChangeResource rsrc) {
+ return new UiAction.Description()
+ .setLabel("Mark Unreviewed")
+ .setTitle("Mark the change as unreviewed to highlight it in the dashboard")
+ .setVisible(isReviewed(rsrc));
+ }
+
+ @Override
+ public Response<String> apply(ChangeResource rsrc, Input input)
+ throws OrmException, IllegalLabelException {
+ stars.markAsUnreviewed(rsrc);
+ return Response.ok("");
+ }
+
+ private boolean isReviewed(ChangeResource rsrc) {
+ try {
+ return changeDataFactory
+ .create(dbProvider.get(), rsrc.getNotes())
+ .isReviewedBy(rsrc.getUser().asIdentifiedUser().getAccountId());
+ } catch (OrmException e) {
+ log.error("failed to check if change is reviewed", e);
+ }
+ return false;
+ }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/Module.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/Module.java
index f3a6c66..f648d5a 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/Module.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/Module.java
@@ -90,8 +90,8 @@
delete(CHANGE_KIND, "private").to(DeletePrivate.class);
put(CHANGE_KIND, "ignore").to(Ignore.class);
put(CHANGE_KIND, "unignore").to(Unignore.class);
- put(CHANGE_KIND, "mute").to(Mute.class);
- put(CHANGE_KIND, "unmute").to(Unmute.class);
+ put(CHANGE_KIND, "reviewed").to(MarkAsReviewed.class);
+ put(CHANGE_KIND, "unreviewed").to(MarkAsUnreviewed.class);
post(CHANGE_KIND, "wip").to(SetWorkInProgress.class);
post(CHANGE_KIND, "ready").to(SetReadyForReview.class);
put(CHANGE_KIND, "message").to(PutMessage.class);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/Mute.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/Mute.java
deleted file mode 100644
index 9da993b..0000000
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/Mute.java
+++ /dev/null
@@ -1,80 +0,0 @@
-// Copyright (C) 2017 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.server.change;
-
-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.extensions.webui.UiAction;
-import com.google.gerrit.server.StarredChangesUtil;
-import com.google.gerrit.server.StarredChangesUtil.IllegalLabelException;
-import com.google.gwtorm.server.OrmException;
-import com.google.inject.Inject;
-import com.google.inject.Singleton;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-@Singleton
-public class Mute implements RestModifyView<ChangeResource, Mute.Input>, UiAction<ChangeResource> {
- private static final Logger log = LoggerFactory.getLogger(Mute.class);
-
- public static class Input {}
-
- private final StarredChangesUtil stars;
-
- @Inject
- Mute(StarredChangesUtil stars) {
- this.stars = stars;
- }
-
- @Override
- public Description getDescription(ChangeResource rsrc) {
- return new UiAction.Description()
- .setLabel("Mute")
- .setTitle("Mute the change to unhighlight it in the dashboard")
- .setVisible(isMuteable(rsrc));
- }
-
- @Override
- public Response<String> apply(ChangeResource rsrc, Input input)
- throws RestApiException, OrmException, IllegalLabelException {
- if (rsrc.isUserOwner()) {
- throw new BadRequestException("cannot mute own change");
- }
- if (!isMuted(rsrc)) {
- stars.mute(rsrc);
- }
- return Response.ok("");
- }
-
- private boolean isMuted(ChangeResource rsrc) {
- try {
- return stars.isMuted(rsrc);
- } catch (OrmException e) {
- log.error("failed to check muted star", e);
- }
- return false;
- }
-
- private boolean isMuteable(ChangeResource rsrc) {
- try {
- return !rsrc.isUserOwner() && !isMuted(rsrc) && !stars.isIgnored(rsrc);
- } catch (OrmException e) {
- log.error("failed to check ignored star", e);
- }
- return false;
- }
-}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/NotifyUtil.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/NotifyUtil.java
index ccc7587..c29faee 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/NotifyUtil.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/NotifyUtil.java
@@ -25,11 +25,9 @@
import com.google.gerrit.extensions.api.changes.RecipientType;
import com.google.gerrit.extensions.restapi.BadRequestException;
import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gerrit.server.account.AccountResolver;
import com.google.gwtorm.server.OrmException;
import com.google.inject.Inject;
-import com.google.inject.Provider;
import com.google.inject.Singleton;
import java.io.IOException;
import java.util.ArrayList;
@@ -40,12 +38,10 @@
@Singleton
public class NotifyUtil {
- private final Provider<ReviewDb> dbProvider;
private final AccountResolver accountResolver;
@Inject
- NotifyUtil(Provider<ReviewDb> dbProvider, AccountResolver accountResolver) {
- this.dbProvider = dbProvider;
+ NotifyUtil(AccountResolver accountResolver) {
this.accountResolver = accountResolver;
}
@@ -90,19 +86,19 @@
if (m == null) {
m = MultimapBuilder.hashKeys().arrayListValues().build();
}
- m.putAll(e.getKey(), find(dbProvider.get(), accounts));
+ m.putAll(e.getKey(), find(accounts));
}
}
return m != null ? m : ImmutableListMultimap.of();
}
- private List<Account.Id> find(ReviewDb db, List<String> nameOrEmails)
+ private List<Account.Id> find(List<String> nameOrEmails)
throws OrmException, BadRequestException, IOException, ConfigInvalidException {
List<String> missing = new ArrayList<>(nameOrEmails.size());
List<Account.Id> r = new ArrayList<>(nameOrEmails.size());
for (String nameOrEmail : nameOrEmails) {
- Account a = accountResolver.find(db, nameOrEmail);
+ Account a = accountResolver.find(nameOrEmail);
if (a != null) {
r.add(a.getId());
} else {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/Unmute.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/Unmute.java
deleted file mode 100644
index 16d6d88..0000000
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/Unmute.java
+++ /dev/null
@@ -1,67 +0,0 @@
-// Copyright (C) 2017 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.server.change;
-
-import com.google.gerrit.extensions.restapi.Response;
-import com.google.gerrit.extensions.restapi.RestModifyView;
-import com.google.gerrit.extensions.webui.UiAction;
-import com.google.gerrit.server.StarredChangesUtil;
-import com.google.gerrit.server.StarredChangesUtil.IllegalLabelException;
-import com.google.gwtorm.server.OrmException;
-import com.google.inject.Inject;
-import com.google.inject.Singleton;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-@Singleton
-public class Unmute
- implements RestModifyView<ChangeResource, Unmute.Input>, UiAction<ChangeResource> {
- private static final Logger log = LoggerFactory.getLogger(Unmute.class);
-
- public static class Input {}
-
- private final StarredChangesUtil stars;
-
- @Inject
- Unmute(StarredChangesUtil stars) {
- this.stars = stars;
- }
-
- @Override
- public Description getDescription(ChangeResource rsrc) {
- return new UiAction.Description()
- .setLabel("Unmute")
- .setTitle("Unmute the change")
- .setVisible(isMuted(rsrc));
- }
-
- @Override
- public Response<String> apply(ChangeResource rsrc, Input input)
- throws OrmException, IllegalLabelException {
- if (isMuted(rsrc)) {
- stars.unmute(rsrc);
- }
- return Response.ok("");
- }
-
- private boolean isMuted(ChangeResource rsrc) {
- try {
- return stars.isMuted(rsrc);
- } catch (OrmException e) {
- log.error("failed to check muted star", e);
- }
- return false;
- }
-}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/GerritGlobalModule.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/GerritGlobalModule.java
index 901084e..0e4e8b4 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/config/GerritGlobalModule.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/GerritGlobalModule.java
@@ -81,6 +81,7 @@
import com.google.gerrit.server.Sequences;
import com.google.gerrit.server.account.AccountCacheImpl;
import com.google.gerrit.server.account.AccountControl;
+import com.google.gerrit.server.account.AccountDeactivator;
import com.google.gerrit.server.account.AccountManager;
import com.google.gerrit.server.account.AccountResolver;
import com.google.gerrit.server.account.AccountVisibilityProvider;
@@ -279,6 +280,7 @@
bind(GcConfig.class);
bind(ChangeCleanupConfig.class);
+ bind(AccountDeactivator.class);
bind(ApprovalsUtil.class);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/receive/ReceiveCommits.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/receive/ReceiveCommits.java
index 3c8bcea..b7aa416 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/receive/ReceiveCommits.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/receive/ReceiveCommits.java
@@ -2126,7 +2126,7 @@
checkNotNull(magicBranch);
recipients.add(magicBranch.getMailRecipients());
approvals = magicBranch.labels;
- recipients.add(getRecipientsFromFooters(db, accountResolver, footerLines));
+ recipients.add(getRecipientsFromFooters(accountResolver, footerLines));
recipients.remove(me);
StringBuilder msg =
new StringBuilder(
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/receive/ReplaceOp.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/receive/ReplaceOp.java
index bcb8564..4455aed 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/receive/ReplaceOp.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/receive/ReplaceOp.java
@@ -291,7 +291,7 @@
psDescription);
update.setPsDescription(psDescription);
- recipients.add(getRecipientsFromFooters(ctx.getDb(), accountResolver, commit.getFooterLines()));
+ recipients.add(getRecipientsFromFooters(accountResolver, commit.getFooterLines()));
recipients.remove(ctx.getAccountId());
ChangeData cd = changeDataFactory.create(ctx.getDb(), ctx.getNotes());
MailRecipients oldRecipients = getRecipientsFromReviewers(cd.reviewers());
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/group/AddMembers.java b/gerrit-server/src/main/java/com/google/gerrit/server/group/AddMembers.java
index 96024d2..b6bcb3b 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/group/AddMembers.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/group/AddMembers.java
@@ -152,7 +152,7 @@
case HTTP_LDAP:
case CLIENT_SSL_CERT_LDAP:
case LDAP:
- if (accountResolver.find(db.get(), nameOrEmailOrId) == null) {
+ if (accountResolver.find(nameOrEmailOrId) == null) {
// account does not exist, try to create it
Account a = createAccountByLdap(nameOrEmailOrId);
if (a != null) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/index/change/ChangeSchemaDefinitions.java b/gerrit-server/src/main/java/com/google/gerrit/server/index/change/ChangeSchemaDefinitions.java
index c103c89..95bdaab 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/index/change/ChangeSchemaDefinitions.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/index/change/ChangeSchemaDefinitions.java
@@ -91,7 +91,10 @@
@Deprecated static final Schema<ChangeData> V46 = schema(V45);
// Removal of draft change workflow requires reindexing
- static final Schema<ChangeData> V47 = schema(V46);
+ @Deprecated static final Schema<ChangeData> V47 = schema(V46);
+
+ // Rename of star label 'mute' to 'reviewed' requires reindexing
+ static final Schema<ChangeData> V48 = schema(V47);
public static final String NAME = "changes";
public static final ChangeSchemaDefinitions INSTANCE = new ChangeSchemaDefinitions();
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/MailUtil.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/MailUtil.java
index be1c9f5..0487cc0 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/MailUtil.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/MailUtil.java
@@ -20,7 +20,6 @@
import com.google.gerrit.common.FooterConstants;
import com.google.gerrit.common.errors.NoSuchAccountException;
import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gerrit.server.ReviewerSet;
import com.google.gerrit.server.account.AccountResolver;
import com.google.gwtorm.server.OrmException;
@@ -39,15 +38,15 @@
DateTimeFormatter.ofPattern("EEE, dd MMM yyyy HH:mm:ss ZZZ");
public static MailRecipients getRecipientsFromFooters(
- ReviewDb db, AccountResolver accountResolver, List<FooterLine> footerLines)
+ AccountResolver accountResolver, List<FooterLine> footerLines)
throws OrmException, IOException {
MailRecipients recipients = new MailRecipients();
for (FooterLine footerLine : footerLines) {
try {
if (isReviewer(footerLine)) {
- recipients.reviewers.add(toAccountId(db, accountResolver, footerLine.getValue().trim()));
+ recipients.reviewers.add(toAccountId(accountResolver, footerLine.getValue().trim()));
} else if (footerLine.matches(FooterKey.CC)) {
- recipients.cc.add(toAccountId(db, accountResolver, footerLine.getValue().trim()));
+ recipients.cc.add(toAccountId(accountResolver, footerLine.getValue().trim()));
}
} catch (NoSuchAccountException e) {
continue;
@@ -63,10 +62,9 @@
return recipients;
}
- private static Account.Id toAccountId(
- ReviewDb db, AccountResolver accountResolver, String nameOrEmail)
+ private static Account.Id toAccountId(AccountResolver accountResolver, String nameOrEmail)
throws OrmException, NoSuchAccountException, IOException {
- Account a = accountResolver.findByNameOrEmail(db, nameOrEmail);
+ Account a = accountResolver.findByNameOrEmail(nameOrEmail);
if (a == null) {
throw new NoSuchAccountException("\"" + nameOrEmail + "\" is not registered");
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/CheckAccess.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/CheckAccess.java
index 281e37e..b8d3fbc 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/CheckAccess.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/CheckAccess.java
@@ -24,7 +24,6 @@
import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.reviewdb.client.Branch;
-import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.account.AccountResolver;
import com.google.gerrit.server.permissions.GlobalPermission;
@@ -34,7 +33,6 @@
import com.google.gerrit.server.permissions.RefPermission;
import com.google.gwtorm.server.OrmException;
import com.google.inject.Inject;
-import com.google.inject.Provider;
import com.google.inject.Singleton;
import java.io.IOException;
import javax.servlet.http.HttpServletResponse;
@@ -43,18 +41,15 @@
@Singleton
public class CheckAccess implements RestModifyView<ProjectResource, AccessCheckInput> {
private final AccountResolver accountResolver;
- private final Provider<ReviewDb> db;
private final IdentifiedUser.GenericFactory userFactory;
private final PermissionBackend permissionBackend;
@Inject
CheckAccess(
AccountResolver resolver,
- Provider<ReviewDb> db,
IdentifiedUser.GenericFactory userFactory,
PermissionBackend permissionBackend) {
this.accountResolver = resolver;
- this.db = db;
this.userFactory = userFactory;
this.permissionBackend = permissionBackend;
}
@@ -72,7 +67,7 @@
throw new BadRequestException("input requires 'account'");
}
- Account match = accountResolver.find(db.get(), input.account);
+ Account match = accountResolver.find(input.account);
if (match == null) {
throw new UnprocessableEntityException(
String.format("cannot find account %s", input.account));
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/DashboardsCollection.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/DashboardsCollection.java
index ea2935d..d43a066 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/DashboardsCollection.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/DashboardsCollection.java
@@ -173,7 +173,7 @@
return views;
}
- static DashboardInfo newDashboardInfo(String ref, String path) {
+ public static DashboardInfo newDashboardInfo(String ref, String path) {
DashboardInfo info = new DashboardInfo();
info.ref = ref;
info.path = path;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/DeleteDashboard.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/DeleteDashboard.java
index 7296311..958de55 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/DeleteDashboard.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/DeleteDashboard.java
@@ -16,12 +16,9 @@
import com.google.gerrit.extensions.api.projects.DashboardInfo;
import com.google.gerrit.extensions.common.SetDashboardInput;
-import com.google.gerrit.extensions.restapi.AuthException;
-import com.google.gerrit.extensions.restapi.BadRequestException;
import com.google.gerrit.extensions.restapi.MethodNotAllowedException;
-import com.google.gerrit.extensions.restapi.ResourceConflictException;
-import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
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.permissions.PermissionBackendException;
import com.google.inject.Inject;
@@ -40,9 +37,7 @@
@Override
public Response<DashboardInfo> apply(DashboardResource resource, SetDashboardInput input)
- throws AuthException, BadRequestException, ResourceConflictException,
- ResourceNotFoundException, MethodNotAllowedException, IOException,
- PermissionBackendException {
+ throws RestApiException, IOException, PermissionBackendException {
if (resource.isProjectDefault()) {
SetDashboardInput in = new SetDashboardInput();
in.commitMessage = input != null ? input.commitMessage : null;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/GetDashboard.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/GetDashboard.java
index adca214..cdf23bb 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/GetDashboard.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/GetDashboard.java
@@ -21,9 +21,11 @@
import com.google.common.base.Strings;
import com.google.common.collect.Lists;
import com.google.gerrit.extensions.api.projects.DashboardInfo;
+import com.google.gerrit.extensions.restapi.BadRequestException;
import com.google.gerrit.extensions.restapi.IdString;
import com.google.gerrit.extensions.restapi.ResourceConflictException;
import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
+import com.google.gerrit.extensions.restapi.RestApiException;
import com.google.gerrit.extensions.restapi.RestReadView;
import com.google.gerrit.extensions.restapi.Url;
import com.google.gerrit.server.permissions.PermissionBackendException;
@@ -51,11 +53,9 @@
@Override
public DashboardInfo apply(DashboardResource resource)
- throws ResourceNotFoundException, ResourceConflictException, IOException,
- PermissionBackendException {
+ throws RestApiException, IOException, PermissionBackendException {
if (inherited && !resource.isProjectDefault()) {
- // inherited flag can only be used with default.
- throw new ResourceNotFoundException("inherited");
+ throw new BadRequestException("inherited flag can only be used with default");
}
String project = resource.getControl().getProject().getName();
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/ListDashboards.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/ListDashboards.java
index 1d3c58c..6960b47 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/ListDashboards.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/ListDashboards.java
@@ -46,7 +46,7 @@
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
-class ListDashboards implements RestReadView<ProjectResource> {
+public class ListDashboards implements RestReadView<ProjectResource> {
private static final Logger log = LoggerFactory.getLogger(ListDashboards.class);
private final GitRepositoryManager gitManager;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/SetDashboard.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/SetDashboard.java
index 9222322..21ec077 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/SetDashboard.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/SetDashboard.java
@@ -16,12 +16,9 @@
import com.google.gerrit.extensions.api.projects.DashboardInfo;
import com.google.gerrit.extensions.common.SetDashboardInput;
-import com.google.gerrit.extensions.restapi.AuthException;
-import com.google.gerrit.extensions.restapi.BadRequestException;
import com.google.gerrit.extensions.restapi.MethodNotAllowedException;
-import com.google.gerrit.extensions.restapi.ResourceConflictException;
-import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
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.permissions.PermissionBackendException;
import com.google.inject.Inject;
@@ -30,7 +27,7 @@
import java.io.IOException;
@Singleton
-class SetDashboard implements RestModifyView<DashboardResource, SetDashboardInput> {
+public class SetDashboard implements RestModifyView<DashboardResource, SetDashboardInput> {
private final Provider<SetDefaultDashboard> defaultSetter;
@Inject
@@ -40,9 +37,7 @@
@Override
public Response<DashboardInfo> apply(DashboardResource resource, SetDashboardInput input)
- throws AuthException, BadRequestException, ResourceConflictException,
- MethodNotAllowedException, ResourceNotFoundException, IOException,
- PermissionBackendException {
+ throws RestApiException, IOException, PermissionBackendException {
if (resource.isProjectDefault()) {
return defaultSetter.get().apply(resource, input);
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/SetDefaultDashboard.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/SetDefaultDashboard.java
index 256b6f2..9aa9ae7 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/SetDefaultDashboard.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/SetDefaultDashboard.java
@@ -24,6 +24,7 @@
import com.google.gerrit.extensions.restapi.ResourceConflictException;
import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
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.reviewdb.client.Project;
import com.google.gerrit.server.git.MetaDataUpdate;
@@ -59,8 +60,7 @@
@Override
public Response<DashboardInfo> apply(DashboardResource resource, SetDashboardInput input)
- throws AuthException, BadRequestException, ResourceConflictException,
- ResourceNotFoundException, IOException, PermissionBackendException {
+ throws RestApiException, IOException, PermissionBackendException {
if (input == null) {
input = new SetDashboardInput(); // Delete would set input to null.
}
@@ -132,8 +132,7 @@
@Override
public Response<DashboardInfo> apply(ProjectResource resource, SetDashboardInput input)
- throws AuthException, BadRequestException, ResourceConflictException,
- ResourceNotFoundException, IOException, PermissionBackendException {
+ throws RestApiException, IOException, PermissionBackendException {
SetDefaultDashboard set = setDefault.get();
set.inherited = inherited;
return set.apply(DashboardResource.projectDefault(resource.getControl()), input);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeData.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeData.java
index 40b384d..2a71258 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeData.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeData.java
@@ -1084,6 +1084,22 @@
return draftsByUser;
}
+ public boolean isReviewedBy(Account.Id accountId) throws OrmException {
+ Collection<String> stars = stars(accountId);
+
+ if (stars.contains(
+ StarredChangesUtil.REVIEWED_LABEL + "/" + currentPatchSet().getPatchSetId())) {
+ return true;
+ }
+
+ if (stars.contains(
+ StarredChangesUtil.UNREVIEWED_LABEL + "/" + currentPatchSet().getPatchSetId())) {
+ return false;
+ }
+
+ return reviewedBy().contains(accountId);
+ }
+
public Set<Account.Id> reviewedBy() throws OrmException {
if (reviewedBy == null) {
if (!lazyLoad) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java
index beff5f2..1ae579e 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java
@@ -901,7 +901,7 @@
if (isSelf(who)) {
return is_visible();
}
- Set<Account.Id> m = args.accountResolver.findAll(args.db.get(), who);
+ Set<Account.Id> m = args.accountResolver.findAll(who);
if (!m.isEmpty()) {
List<Predicate<ChangeData>> p = Lists.newArrayListWithCapacity(m.size());
for (Account.Id id : m) {
@@ -1258,7 +1258,7 @@
if (isSelf(who)) {
return Collections.singleton(self());
}
- Set<Account.Id> matches = args.accountResolver.findAll(args.db.get(), who);
+ Set<Account.Id> matches = args.accountResolver.findAll(who);
if (matches.isEmpty()) {
throw error("User " + who + " not found");
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/group/GroupQueryBuilder.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/group/GroupQueryBuilder.java
index 6bd6e24..057cc44 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/group/GroupQueryBuilder.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/group/GroupQueryBuilder.java
@@ -27,7 +27,6 @@
import com.google.gerrit.index.query.QueryParseException;
import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.reviewdb.client.AccountGroup;
-import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gerrit.server.account.AccountResolver;
import com.google.gerrit.server.account.GroupBackend;
import com.google.gerrit.server.account.GroupBackends;
@@ -38,7 +37,6 @@
import com.google.gerrit.server.index.group.GroupIndexCollection;
import com.google.gwtorm.server.OrmException;
import com.google.inject.Inject;
-import com.google.inject.Provider;
import java.io.IOException;
import java.util.List;
import java.util.Optional;
@@ -58,7 +56,6 @@
new QueryBuilder.Definition<>(GroupQueryBuilder.class);
public static class Arguments {
- final Provider<ReviewDb> db;
final GroupIndex groupIndex;
final GroupCache groupCache;
final GroupBackend groupBackend;
@@ -66,12 +63,10 @@
@Inject
Arguments(
- Provider<ReviewDb> db,
GroupIndexCollection groupIndexCollection,
GroupCache groupCache,
GroupBackend groupBackend,
AccountResolver accountResolver) {
- this.db = db;
this.groupIndex = groupIndexCollection.getSearchIndex();
this.groupCache = groupCache;
this.groupBackend = groupBackend;
@@ -189,7 +184,7 @@
private Set<Account.Id> parseAccount(String nameOrEmail)
throws QueryParseException, OrmException, IOException, ConfigInvalidException {
- Set<Account.Id> foundAccounts = args.accountResolver.findAll(args.db.get(), nameOrEmail);
+ Set<Account.Id> foundAccounts = args.accountResolver.findAll(nameOrEmail);
if (foundAccounts.isEmpty()) {
throw error("User " + nameOrEmail + " not found");
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/SchemaVersion.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/SchemaVersion.java
index e92b003..d1cbad6 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/SchemaVersion.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/SchemaVersion.java
@@ -35,7 +35,7 @@
/** A version of the database schema. */
public abstract class SchemaVersion {
/** The current schema version. */
- public static final Class<Schema_160> C = Schema_160.class;
+ public static final Class<Schema_161> C = Schema_161.class;
public static int getBinaryVersion() {
return guessVersion(C);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_161.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_161.java
new file mode 100644
index 0000000..407492d
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_161.java
@@ -0,0 +1,76 @@
+// Copyright (C) 2017 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.schema;
+
+import static java.util.stream.Collectors.toList;
+
+import com.google.gerrit.reviewdb.client.RefNames;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.StarredChangesUtil;
+import com.google.gerrit.server.StarredChangesUtil.IllegalLabelException;
+import com.google.gerrit.server.StarredChangesUtil.StarRef;
+import com.google.gerrit.server.config.AllUsersName;
+import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import java.io.IOException;
+import org.eclipse.jgit.lib.BatchRefUpdate;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.Ref;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.lib.TextProgressMonitor;
+import org.eclipse.jgit.revwalk.RevWalk;
+import org.eclipse.jgit.transport.ReceiveCommand;
+
+public class Schema_161 extends SchemaVersion {
+ private static final String MUTE_LABEL = "mute";
+
+ private final GitRepositoryManager repoManager;
+ private final AllUsersName allUsersName;
+
+ @Inject
+ Schema_161(
+ Provider<Schema_160> prior, GitRepositoryManager repoManager, AllUsersName allUsersName) {
+ super(prior);
+ this.repoManager = repoManager;
+ this.allUsersName = allUsersName;
+ }
+
+ @Override
+ protected void migrateData(ReviewDb db, UpdateUI ui) throws OrmException {
+ try (Repository git = repoManager.openRepository(allUsersName);
+ RevWalk rw = new RevWalk(git)) {
+ BatchRefUpdate bru = git.getRefDatabase().newBatchUpdate();
+ for (Ref ref : git.getRefDatabase().getRefs(RefNames.REFS_STARRED_CHANGES).values()) {
+ StarRef starRef = StarredChangesUtil.readLabels(git, ref.getName());
+ if (starRef.labels().contains(MUTE_LABEL)) {
+ ObjectId id =
+ StarredChangesUtil.writeLabels(
+ git,
+ starRef
+ .labels()
+ .stream()
+ .map(l -> l.equals(MUTE_LABEL) ? StarredChangesUtil.REVIEWED_LABEL : l)
+ .collect(toList()));
+ bru.addCommand(new ReceiveCommand(ObjectId.zeroId(), id, ref.getName()));
+ }
+ }
+ bru.execute(rw, new TextProgressMonitor());
+ } catch (IOException | IllegalLabelException ex) {
+ throw new OrmException(ex);
+ }
+ }
+}
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/LsUserRefs.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/LsUserRefs.java
index bbe736d..275da7c 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/LsUserRefs.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/LsUserRefs.java
@@ -22,7 +22,6 @@
import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.reviewdb.client.Project;
import com.google.gerrit.reviewdb.client.RefNames;
-import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gerrit.server.account.AccountResolver;
import com.google.gerrit.server.git.GitRepositoryManager;
import com.google.gerrit.server.git.VisibleRefFilter;
@@ -51,7 +50,6 @@
@Inject private AccountResolver accountResolver;
@Inject private OneOffRequestContext requestContext;
@Inject private VisibleRefFilter.Factory refFilterFactory;
- @Inject private ReviewDb db;
@Inject private GitRepositoryManager repoManager;
@Option(
@@ -79,7 +77,7 @@
protected void run() throws Failure {
Account userAccount;
try {
- userAccount = accountResolver.find(db, userName);
+ userAccount = accountResolver.find(userName);
} catch (OrmException | IOException | ConfigInvalidException e) {
throw die(e);
}
diff --git a/gerrit-war/src/main/java/com/google/gerrit/httpd/WebAppInitializer.java b/gerrit-war/src/main/java/com/google/gerrit/httpd/WebAppInitializer.java
index 2d4c1d1..9a02fcd 100644
--- a/gerrit-war/src/main/java/com/google/gerrit/httpd/WebAppInitializer.java
+++ b/gerrit-war/src/main/java/com/google/gerrit/httpd/WebAppInitializer.java
@@ -33,6 +33,7 @@
import com.google.gerrit.pgm.util.LogFileCompressor;
import com.google.gerrit.server.LibModuleLoader;
import com.google.gerrit.server.StartupChecks;
+import com.google.gerrit.server.account.AccountDeactivator;
import com.google.gerrit.server.account.InternalAccountDirectory;
import com.google.gerrit.server.cache.h2.DefaultCacheFactory;
import com.google.gerrit.server.change.ChangeCleanupRunner;
@@ -365,6 +366,7 @@
});
modules.add(new GarbageCollectionModule());
modules.add(new ChangeCleanupRunner.Module());
+ modules.add(new AccountDeactivator.Module());
modules.addAll(LibModuleLoader.loadModules(cfgInjector));
return cfgInjector.createChildInjector(modules);
}
diff --git a/polygerrit-ui/app/behaviors/async-foreach-behavior/async-foreach-behavior.html b/polygerrit-ui/app/behaviors/async-foreach-behavior/async-foreach-behavior.html
new file mode 100644
index 0000000..0148377
--- /dev/null
+++ b/polygerrit-ui/app/behaviors/async-foreach-behavior/async-foreach-behavior.html
@@ -0,0 +1,37 @@
+<!--
+Copyright (C) 2017 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+
+<script>
+(function(window) {
+ 'use strict';
+
+ window.Gerrit = window.Gerrit || {};
+
+ /** @polymerBehavior Gerrit.AsyncForeachBehavior */
+ Gerrit.AsyncForeachBehavior = {
+ /**
+ * @template T
+ * @param {!Array<T>} array
+ * @param {!Function} fn
+ * @return {!Promise<undefined>}
+ */
+ asyncForeach(array, fn) {
+ if (!array.length) { return Promise.resolve(); }
+ return fn(array[0]).then(() => this.asyncForeach(array.slice(1), fn));
+ },
+ };
+})(window);
+</script>
diff --git a/polygerrit-ui/app/behaviors/async-foreach-behavior/async-foreach-behavior_test.html b/polygerrit-ui/app/behaviors/async-foreach-behavior/async-foreach-behavior_test.html
new file mode 100644
index 0000000..ba15ad7
--- /dev/null
+++ b/polygerrit-ui/app/behaviors/async-foreach-behavior/async-foreach-behavior_test.html
@@ -0,0 +1,39 @@
+<!DOCTYPE html>
+<!--
+Copyright (C) 2017 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+
+<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
+<title>async-foreach-behavior</title>
+
+<script src="../../bower_components/webcomponentsjs/webcomponents.min.js"></script>
+<script src="../../bower_components/web-component-tester/browser.js"></script>
+<link rel="import" href="../../test/common-test-setup.html"/>
+<link rel="import" href="async-foreach-behavior.html">
+
+<script>
+ suite('async-foreach-behavior tests', () => {
+ test('loops over each item', () => {
+ const fn = sinon.stub().returns(Promise.resolve());
+ return Gerrit.AsyncForeachBehavior.asyncForeach([1, 2, 3], fn)
+ .then(() => {
+ assert.isTrue(fn.calledThrice);
+ assert.equal(fn.getCall(0).args[0], 1);
+ assert.equal(fn.getCall(1).args[0], 2);
+ assert.equal(fn.getCall(2).args[0], 3);
+ });
+ });
+ });
+</script>
diff --git a/polygerrit-ui/app/behaviors/gr-patch-set-behavior/gr-patch-set-behavior.html b/polygerrit-ui/app/behaviors/gr-patch-set-behavior/gr-patch-set-behavior.html
index 9936730..13c232e 100644
--- a/polygerrit-ui/app/behaviors/gr-patch-set-behavior/gr-patch-set-behavior.html
+++ b/polygerrit-ui/app/behaviors/gr-patch-set-behavior/gr-patch-set-behavior.html
@@ -156,7 +156,6 @@
};
});
}
- patchNums.sort((a, b) => { return a.num - b.num; });
return Gerrit.PatchSetBehavior._computeWipForPatchSets(change, patchNums);
},
diff --git a/polygerrit-ui/app/behaviors/gr-path-list-behavior/gr-path-list-behavior.html b/polygerrit-ui/app/behaviors/gr-path-list-behavior/gr-path-list-behavior.html
index d63b961..f3db039 100644
--- a/polygerrit-ui/app/behaviors/gr-path-list-behavior/gr-path-list-behavior.html
+++ b/polygerrit-ui/app/behaviors/gr-path-list-behavior/gr-path-list-behavior.html
@@ -13,6 +13,7 @@
See the License for the specific language governing permissions and
limitations under the License.
-->
+<script src="../../scripts/util.js"></script>
<script>
(function(window) {
'use strict';
@@ -20,6 +21,10 @@
window.Gerrit = window.Gerrit || {};
/** @polymerBehavior Gerrit.PathListBehavior */
Gerrit.PathListBehavior = {
+
+ COMMIT_MESSAGE_PATH: '/COMMIT_MSG',
+ MERGE_LIST_PATH: '/MERGE_LIST',
+
/**
* @param {string} a
* @param {string} b
@@ -27,20 +32,18 @@
*/
specialFilePathCompare(a, b) {
// The commit message always goes first.
- const COMMIT_MESSAGE_PATH = '/COMMIT_MSG';
- if (a === COMMIT_MESSAGE_PATH) {
+ if (a === Gerrit.PathListBehavior.COMMIT_MESSAGE_PATH) {
return -1;
}
- if (b === COMMIT_MESSAGE_PATH) {
+ if (b === Gerrit.PathListBehavior.COMMIT_MESSAGE_PATH) {
return 1;
}
// The merge list always comes next.
- const MERGE_LIST_PATH = '/MERGE_LIST';
- if (a === MERGE_LIST_PATH) {
+ if (a === Gerrit.PathListBehavior.MERGE_LIST_PATH) {
return -1;
}
- if (b === MERGE_LIST_PATH) {
+ if (b === Gerrit.PathListBehavior.MERGE_LIST_PATH) {
return 1;
}
@@ -67,6 +70,40 @@
}
return aFile.localeCompare(bFile) || a.localeCompare(b);
},
+
+ computeDisplayPath(path) {
+ if (path === Gerrit.PathListBehavior.COMMIT_MESSAGE_PATH) {
+ return 'Commit message';
+ } else if (path === Gerrit.PathListBehavior.MERGE_LIST_PATH) {
+ return 'Merge list';
+ }
+ return path;
+ },
+
+ computeTruncatedPath(path) {
+ return Gerrit.PathListBehavior.truncatePath(
+ Gerrit.PathListBehavior.computeDisplayPath(path));
+ },
+
+ /**
+ * Truncates URLs to display filename only
+ * Example
+ * // returns '.../text.html'
+ * util.truncatePath.('dir/text.html');
+ * Example
+ * // returns 'text.html'
+ * util.truncatePath.('text.html');
+ * @return {string} Returns the truncated value of a URL.
+ */
+ truncatePath(path) {
+ const pathPieces = path.split('/');
+
+ if (pathPieces.length < 2) {
+ return path;
+ }
+ // Character is an ellipsis.
+ return '\u2026/' + pathPieces[pathPieces.length - 1];
+ },
};
})(window);
</script>
diff --git a/polygerrit-ui/app/behaviors/gr-path-list-behavior/gr-path-list-behavior_test.html b/polygerrit-ui/app/behaviors/gr-path-list-behavior/gr-path-list-behavior_test.html
index 37772d1..e0b1b7e 100644
--- a/polygerrit-ui/app/behaviors/gr-path-list-behavior/gr-path-list-behavior_test.html
+++ b/polygerrit-ui/app/behaviors/gr-path-list-behavior/gr-path-list-behavior_test.html
@@ -44,5 +44,34 @@
'/mrPeanutbutter.py',
]);
});
+
+ test('file display name', () => {
+ const name = Gerrit.PathListBehavior.computeDisplayPath;
+ assert.equal(name('/foo/bar/baz'), '/foo/bar/baz');
+ assert.equal(name('/foobarbaz'), '/foobarbaz');
+ assert.equal(name('/COMMIT_MSG'), 'Commit message');
+ assert.equal(name('/MERGE_LIST'), 'Merge list');
+ });
+
+ test('truncatePath with long path should add ellipsis', () => {
+ const truncatePath = Gerrit.PathListBehavior.truncatePath;
+ let path = 'level1/level2/level3/level4/file.js';
+ let shortenedPath = truncatePath(path);
+ // The expected path is truncated with an ellipsis.
+ const expectedPath = '\u2026/file.js';
+ assert.equal(shortenedPath, expectedPath);
+
+ path = 'level2/file.js';
+ shortenedPath = truncatePath(path);
+ assert.equal(shortenedPath, expectedPath);
+ });
+
+ test('truncatePath with short path should not add ellipsis', () => {
+ const truncatePath = Gerrit.PathListBehavior.truncatePath;
+ const path = 'file.js';
+ const expectedPath = 'file.js';
+ const shortenedPath = truncatePath(path);
+ assert.equal(shortenedPath, expectedPath);
+ });
});
</script>
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view.html b/polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view.html
index b86d0f7..d79dc69 100644
--- a/polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view.html
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view.html
@@ -20,6 +20,7 @@
<link rel="import" href="../../core/gr-navigation/gr-navigation.html">
<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
<link rel="import" href="../gr-change-list/gr-change-list.html">
+<link rel="import" href="../gr-user-header/gr-user-header.html">
<link rel="import" href="../../../styles/shared-styles.html">
<dom-module id="gr-change-list-view">
@@ -46,6 +47,9 @@
nav a:first-of-type {
margin-right: .5em;
}
+ .hide {
+ display: none;
+ }
@media only screen and (max-width: 50em) {
.loading,
.error {
@@ -55,6 +59,9 @@
</style>
<div class="loading" hidden$="[[!_loading]]" hidden>Loading...</div>
<div hidden$="[[_loading]]" hidden>
+ <gr-user-header
+ user-id="[[_userId]]"
+ class$="[[_computeUserHeaderClass(_userId)]]"></gr-user-header>
<gr-change-list
changes="{{_changes}}"
selected-index="{{viewState.selectedChangeIndex}}"
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view.js b/polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view.js
index 30fc679..cc35ff8 100644
--- a/polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view.js
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view.js
@@ -19,6 +19,8 @@
CHANGE_NUM: /^\s*[1-9][0-9]*\s*$/g,
};
+ const USER_QUERY_PATTERN = /^owner:\s?("[^"]+"|[^ ]+)$/;
+
const LIMIT_OPERATOR_PATTERN = /\blimit:(\d+)/i;
Polymer({
@@ -84,7 +86,10 @@
/**
* Change objects loaded from the server.
*/
- _changes: Array,
+ _changes: {
+ type: Array,
+ observer: '_changesChanged',
+ },
/**
* For showing a "loading..." string during ajax requests.
@@ -93,6 +98,12 @@
type: Boolean,
value: true,
},
+
+ /** @type {?String} */
+ _userId: {
+ type: String,
+ value: null,
+ },
},
listeners: {
@@ -188,5 +199,18 @@
page.show(this._computeNavLink(
this._query, this._offset, -1, this._changesPerPage));
},
+
+ _changesChanged(changes) {
+ if (!changes || !changes.length ||
+ !USER_QUERY_PATTERN.test(this._query)) {
+ this._userId = null;
+ return;
+ }
+ this._userId = changes[0].owner.email;
+ },
+
+ _computeUserHeaderClass(userId) {
+ return userId ? '' : 'hide';
+ },
});
})();
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view_test.html b/polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view_test.html
index d70f0c9..5a28565 100644
--- a/polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view_test.html
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view_test.html
@@ -167,6 +167,21 @@
assert.isTrue(showStub.called);
});
+ test('_userId query', done => {
+ assert.isNull(element._userId);
+ element._query = 'owner: foo@bar';
+ element._changes = [{owner: {email: 'foo@bar'}}];
+ flush(() => {
+ assert.equal(element._userId, 'foo@bar');
+
+ element._query = 'foo bar baz';
+ element._changes = [{owner: {email: 'foo@bar'}}];
+ assert.isNull(element._userId);
+
+ done();
+ });
+ });
+
suite('query based navigation', () => {
test('Searching for a change ID redirects to change', done => {
sandbox.stub(element, '_getChanges')
diff --git a/polygerrit-ui/app/elements/change-list/gr-user-header/gr-user-header.html b/polygerrit-ui/app/elements/change-list/gr-user-header/gr-user-header.html
new file mode 100644
index 0000000..9a7ca33
--- /dev/null
+++ b/polygerrit-ui/app/elements/change-list/gr-user-header/gr-user-header.html
@@ -0,0 +1,88 @@
+<!--
+Copyright (C) 2017 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+
+<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="../../shared/gr-avatar/gr-avatar.html">
+<link rel="import" href="../../shared/gr-date-formatter/gr-date-formatter.html">
+<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
+<link rel="import" href="../../../styles/shared-styles.html">
+
+<dom-module id="gr-user-header">
+ <template>
+ <style include="shared-styles">
+ :host {
+ display: block;
+ height: 9em;
+ position: relative;
+ width: 100%;
+ }
+ gr-avatar {
+ height: 7em;
+ left: 1em;
+ position: absolute;
+ top: 1em;
+ width: 7em;
+ }
+ .info {
+ left: 9em;
+ position: absolute;
+ top: 1em;
+ }
+ .info > div > span {
+ display: inline-block;
+ font-weight: bold;
+ text-align: right;
+ width: 6em;
+ }
+ .name {
+ margin-bottom: .25em;
+ }
+ .name hr {
+ width: 100%;
+ }
+ .status.hide,
+ .name.hide {
+ display: none;
+ }
+ </style>
+ <gr-avatar
+ account="[[_accountDetails]]"
+ image-size="100"
+ aria-label="Account avatar"></gr-avatar>
+ <div class="info">
+ <h1 class$="name">
+ [[_computeDetail(_accountDetails, 'name')]]
+ <hr/>
+ </h1>
+ <div class$="status [[_computeStatusClass(_accountDetails)]]">
+ <span>Status:</span> [[_status]]
+ </div>
+ <div>
+ <span>Email:</span>
+ <a href="mailto:[[_computeDetail(_accountDetails, 'email')]]"><!--
+ -->[[_computeDetail(_accountDetails, 'email')]]</a>
+ </div>
+ <div>
+ <span>Joined:</span>
+ <gr-date-formatter
+ date-str="[[_computeDetail(_accountDetails, 'registered_on')]]">
+ </gr-date-formatter>
+ </div>
+ </div>
+ <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
+ </template>
+ <script src="gr-user-header.js"></script>
+</dom-module>
diff --git a/polygerrit-ui/app/elements/change-list/gr-user-header/gr-user-header.js b/polygerrit-ui/app/elements/change-list/gr-user-header/gr-user-header.js
new file mode 100644
index 0000000..dd3512a
--- /dev/null
+++ b/polygerrit-ui/app/elements/change-list/gr-user-header/gr-user-header.js
@@ -0,0 +1,68 @@
+// Copyright (C) 2017 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+(function() {
+ 'use strict';
+
+ Polymer({
+ is: 'gr-user-header',
+ properties: {
+ /** @type {?String} */
+ userId: {
+ type: String,
+ observer: '_accountChanged',
+ },
+
+ /**
+ * @type {?{name: ?, email: ?, registered_on: ?}}
+ */
+ _accountDetails: {
+ type: Object,
+ value: null,
+ },
+
+ /** @type {?String} */
+ _status: {
+ type: String,
+ value: null,
+ },
+ },
+
+ _accountChanged(userId) {
+ if (!userId) {
+ this._accountDetails = null;
+ this._status = null;
+ return;
+ }
+
+ this.$.restAPI.getAccountDetails(userId).then(details => {
+ this._accountDetails = details;
+ });
+ this.$.restAPI.getAccountStatus(userId).then(status => {
+ this._status = status;
+ });
+ },
+
+ _computeDisplayClass(status) {
+ return status ? ' ' : 'hide';
+ },
+
+ _computeDetail(accountDetails, name) {
+ return accountDetails ? accountDetails[name] : '';
+ },
+
+ _computeStatusClass(accountDetails) {
+ return this._computeDetail(accountDetails, 'status') ? '' : 'hide';
+ },
+ });
+})();
diff --git a/polygerrit-ui/app/elements/change-list/gr-user-header/gr-user-header_test.html b/polygerrit-ui/app/elements/change-list/gr-user-header/gr-user-header_test.html
new file mode 100644
index 0000000..ab3b249
--- /dev/null
+++ b/polygerrit-ui/app/elements/change-list/gr-user-header/gr-user-header_test.html
@@ -0,0 +1,72 @@
+<!DOCTYPE html>
+<!--
+Copyright (C) 2017 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+
+<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
+<title>gr-user-header</title>
+
+<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
+<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<link rel="import" href="../../../test/common-test-setup.html"/>
+<link rel="import" href="gr-user-header.html">
+
+<script>void(0);</script>
+
+<test-fixture id="basic">
+ <template>
+ <gr-user-header></gr-user-header>
+ </template>
+</test-fixture>
+
+<script>
+ suite('gr-user-header tests', () => {
+ let element;
+ let sandbox;
+
+ setup(() => {
+ sandbox = sinon.sandbox.create();
+ element = fixture('basic');
+ });
+
+ teardown(() => { sandbox.restore(); });
+
+ test('loads and clears account info', done => {
+ sandbox.stub(element.$.restAPI, 'getAccountDetails')
+ .returns(Promise.resolve({
+ name: 'foo',
+ email: 'bar',
+ registered_on: '2015-03-12 18:32:08.000000000',
+ }));
+ sandbox.stub(element.$.restAPI, 'getAccountStatus')
+ .returns(Promise.resolve('baz'));
+
+ element.userId = 'foo.bar@baz';
+ flush(() => {
+ assert.isOk(element._accountDetails);
+ assert.isOk(element._status);
+
+ element.userId = null;
+ flush(() => {
+ flushAsynchronousOperations();
+ assert.isNull(element._accountDetails);
+ assert.isNull(element._status);
+
+ done();
+ });
+ });
+ });
+ });
+</script>
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 f69e393..8f2468b 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
@@ -54,15 +54,15 @@
DELETE_EDIT: 'deleteEdit',
IGNORE: 'ignore',
MOVE: 'move',
- MUTE: 'mute',
PRIVATE: 'private',
PRIVATE_DELETE: 'private.delete',
PUBLISH_EDIT: 'publishEdit',
REBASE_EDIT: 'rebaseEdit',
RESTORE: 'restore',
REVERT: 'revert',
+ REVIEWED: 'reviewed',
UNIGNORE: 'unignore',
- UNMUTE: 'unmute',
+ UNREVIEWED: 'unreviewed',
WIP: 'wip',
};
@@ -267,11 +267,11 @@
},
{
type: ActionType.CHANGE,
- key: ChangeActions.MUTE,
+ key: ChangeActions.REVIEWED,
},
{
type: ActionType.CHANGE,
- key: ChangeActions.UNMUTE,
+ key: ChangeActions.UNREVIEWED,
},
{
type: ActionType.CHANGE,
@@ -666,7 +666,7 @@
_handleActionTap(e) {
e.preventDefault();
- const el = Polymer.dom(e).rootTarget;
+ const el = e.currentTarget;
const key = el.getAttribute('data-action-key');
if (key.startsWith(ADDITIONAL_ACTION_KEY_PREFIX)) {
this.fire(`${key}-tap`, {node: el});
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 e099170..70d26bf 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
@@ -866,22 +866,22 @@
});
});
- suite('mute change', () => {
+ suite('reviewed change', () => {
setup(done => {
sandbox.stub(element, '_fireAction');
- const MuteAction = {
- __key: 'mute',
+ const ReviewedAction = {
+ __key: 'reviewed',
__type: 'change',
__primary: false,
method: 'PUT',
- label: 'Mute',
+ label: 'Mark reviewed',
title: 'Working...',
enabled: true,
};
element.actions = {
- mute: MuteAction,
+ reviewed: ReviewedAction,
};
element.changeNum = '2';
@@ -890,37 +890,38 @@
element.reload().then(() => { flush(done); });
});
- test('make sure the mute button is not outside of the overflow menu',
+ test('make sure the reviewed button is not outside of the overflow menu',
() => {
- assert.isNotOk(element.$$('[data-action-key="mute"]'));
+ assert.isNotOk(element.$$('[data-action-key="reviewed"]'));
});
- test('muting change', () => {
- assert.isOk(element.$.moreActions.$$('span[data-id="mute-change"]'));
- element.setActionOverflow('change', 'mute', false);
+ test('reviewing change', () => {
+ assert.isOk(
+ element.$.moreActions.$$('span[data-id="reviewed-change"]'));
+ element.setActionOverflow('change', 'reviewed', false);
flushAsynchronousOperations();
- assert.isOk(element.$$('[data-action-key="mute"]'));
+ assert.isOk(element.$$('[data-action-key="reviewed"]'));
assert.isNotOk(
- element.$.moreActions.$$('span[data-id="mute-change"]'));
+ element.$.moreActions.$$('span[data-id="reviewed-change"]'));
});
});
- suite('unmute change', () => {
+ suite('unreviewed change', () => {
setup(done => {
sandbox.stub(element, '_fireAction');
- const UnmuteAction = {
- __key: 'unmute',
+ const UnreviewedAction = {
+ __key: 'unreviewed',
__type: 'change',
__primary: false,
method: 'PUT',
- label: 'Unmute',
+ label: 'Mark unreviewed',
title: 'Working...',
enabled: true,
};
element.actions = {
- unmute: UnmuteAction,
+ unreviewed: UnreviewedAction,
};
element.changeNum = '2';
@@ -930,18 +931,18 @@
});
- test('unmute button not outside of the overflow menu', () => {
- assert.isNotOk(element.$$('[data-action-key="unmute"]'));
+ test('unreviewed button not outside of the overflow menu', () => {
+ assert.isNotOk(element.$$('[data-action-key="unreviewed"]'));
});
- test('unmuting change', () => {
+ test('unreviewed change', () => {
assert.isOk(
- element.$.moreActions.$$('span[data-id="unmute-change"]'));
- element.setActionOverflow('change', 'unmute', false);
+ element.$.moreActions.$$('span[data-id="unreviewed-change"]'));
+ element.setActionOverflow('change', 'unreviewed', false);
flushAsynchronousOperations();
- assert.isOk(element.$$('[data-action-key="unmute"]'));
+ assert.isOk(element.$$('[data-action-key="unreviewed"]'));
assert.isNotOk(
- element.$.moreActions.$$('span[data-id="unmute-change"]'));
+ element.$.moreActions.$$('span[data-id="unreviewed-change"]'));
});
});
diff --git a/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata-it_test.html b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata-it_test.html
index 6129e9b..90776c0 100644
--- a/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata-it_test.html
+++ b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata-it_test.html
@@ -93,7 +93,8 @@
plugin: {
js_resource_paths: [],
html_resource_paths: [
- new URL('test/plugin.html', window.location.href).toString(),
+ new URL('test/plugin.html?' + Math.random(),
+ window.location.href).toString(),
],
},
};
diff --git a/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.html b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.html
index cbaf605..559157d 100644
--- a/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.html
+++ b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.html
@@ -107,6 +107,9 @@
opacity: 0;
pointer-events: none;
}
+ .hashtagChip {
+ margin-bottom: .5em;
+ }
#externalStyle {
display: block;
}
@@ -222,6 +225,7 @@
</template>
<template is="dom-if" if="[[!change.topic]]">
<gr-editable-label
+ uppercase
label-text="Add a topic"
value="[[change.topic]]"
placeholder="[[_computeTopicPlaceholder(_topicReadOnly)]]"
@@ -240,6 +244,7 @@
<span class="value">
<template is="dom-repeat" items="[[change.hashtags]]">
<gr-linked-chip
+ class="hashtagChip"
text="[[item]]"
href="[[_computeHashtagURL(item)]]"
removable="[[!_hashtagReadOnly]]"
@@ -247,6 +252,7 @@
</gr-linked-chip>
</template>
<gr-editable-label
+ uppercase
label-text="Add a hashtag"
value="{{_newHashtag}}"
placeholder="[[_computeHashtagPlaceholder(_hashtagReadOnly)]]"
diff --git a/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.js b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.js
index b5b909a..1fb1c15 100644
--- a/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.js
+++ b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.js
@@ -14,7 +14,7 @@
(function() {
'use strict';
- const HASHTAG_ADD_MESSAGE = 'Click to add';
+ const HASHTAG_ADD_MESSAGE = 'Add Hashtag';
const SubmitTypeLabel = {
FAST_FORWARD_ONLY: 'Fast Forward Only',
@@ -209,7 +209,7 @@
},
_computeTopicPlaceholder(_topicReadOnly) {
- return _topicReadOnly ? 'No Topic' : 'Click to add topic';
+ return _topicReadOnly ? 'No Topic' : 'Add Topic';
},
_computeHashtagPlaceholder(_hashtagReadOnly) {
diff --git a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.html b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.html
index 031acda..f6966683 100644
--- a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.html
+++ b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.html
@@ -205,7 +205,7 @@
margin: 1em var(--default-horizontal-margin);
}
#fileList {
- padding: .5em calc(var(--default-horizontal-margin) / 2);
+ padding: 0 calc(var(--default-horizontal-margin) / 2) .5em;
}
.scrollable {
overflow: auto;
@@ -438,7 +438,8 @@
shown-file-count="[[_shownFileCount]]"
diff-prefs="[[_diffPrefs]]"
diff-view-mode="{{viewState.diffMode}}"
- patch-range="{{_patchRange}}"
+ patch-num="{{_patchNum}}"
+ base-patch-num="{{_basePatchNum}}"
revisions="[[_sortedRevisions]]"
on-open-diff-prefs="_handleOpenDiffPrefs"
on-open-download-dialog="_handleOpenDownloadDialog"
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 31faa54..e143cc1 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
@@ -137,6 +137,16 @@
_patchRange: {
type: Object,
},
+ // These are kept as separate properties from the patchRange so that the
+ // observer can be aware of the previous value. In order to view sub
+ // property changes for _patchRange, a complex observer must be used, and
+ // that only displays the new value.
+ //
+ // If a previous value did not exist, the change is not reloaded with the
+ // new patches. This is just the initial setting from the change view vs.
+ // an update coming from the two way data binding.
+ _patchNum: String,
+ _basePatchNum: String,
_relatedChangesLoading: {
type: Boolean,
value: true,
@@ -211,6 +221,7 @@
'_labelsChanged(_change.labels.*)',
'_paramsAndChangeChanged(params, _change)',
'_updateSortedRevisions(_change.revisions.*)',
+ '_patchRangeChanged(_patchRange.*)',
],
keyBindings: {
@@ -311,6 +322,15 @@
window.location.reload();
},
+ /**
+ * Called when the patch range changes. does not detect sub property
+ * updates.
+ */
+ _patchRangeChanged() {
+ this._basePatchNum = this._patchRange.basePatchNum;
+ this._patchNum = this._patchRange.patchNum;
+ },
+
_handleCommitMessageCancel(e) {
this._editingCommitMessage = false;
},
@@ -501,8 +521,8 @@
if (this._initialLoadComplete && patchChanged) {
if (patchRange.patchNum == null) {
patchRange.patchNum = this.computeLatestPatchNum(this._allPatchSets);
+ this._patchRange = patchRange;
}
- this._patchRange = patchRange;
this._reloadPatchNumDependentResources().then(() => {
this.$.jsAPI.handleEvent(this.$.jsAPI.EventType.SHOW_CHANGE, {
change: this._change,
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 c1b1a90..ec32991 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
@@ -778,6 +778,26 @@
assert.isNull(element._getUrlParameter('test'));
});
+ test('navigateToChange called when range select changes', () => {
+ element._change = {
+ change_id: 'Iad9dc96274af6946f3632be53b106ef80f7ba6ca',
+ revisions: {
+ rev2: {_number: 2},
+ rev1: {_number: 1},
+ rev13: {_number: 13},
+ rev3: {_number: 3},
+ },
+ status: 'NEW',
+ labels: {},
+ };
+ element._basePatchNum = 1;
+ element._patchNum = 2;
+ element._patchNum = 3;
+ assert.equal(navigateToChangeStub.callCount, 1);
+ assert.isTrue(navigateToChangeStub.lastCall
+ .calledWithExactly(element._change, 3, 1));
+ });
+
test('revert dialog opened with revert param', done => {
sandbox.stub(element.$.restAPI, 'getLoggedIn', () => {
return Promise.resolve(true);
@@ -794,6 +814,7 @@
change_id: 'Iad9dc96274af6946f3632be53b106ef80f7ba6ca',
revisions: {
rev1: {_number: 1},
+ rev2: {_number: 2},
},
current_revision: 'rev1',
status: element.ChangeStatus.MERGED,
diff --git a/polygerrit-ui/app/elements/change/gr-comment-list/gr-comment-list.html b/polygerrit-ui/app/elements/change/gr-comment-list/gr-comment-list.html
index 705cf52..e6f37cf 100644
--- a/polygerrit-ui/app/elements/change/gr-comment-list/gr-comment-list.html
+++ b/polygerrit-ui/app/elements/change/gr-comment-list/gr-comment-list.html
@@ -57,7 +57,7 @@
<template is="dom-repeat" items="[[_computeFilesFromComments(comments)]]" as="file">
<div class="file">
<a href$="[[_computeFileDiffURL(file, changeNum, patchNum)]]">
- [[_computeFileDisplayName(file)]]
+ [[computeDisplayPath(file)]]
</a>:
</div>
<template is="dom-repeat"
diff --git a/polygerrit-ui/app/elements/change/gr-comment-list/gr-comment-list.js b/polygerrit-ui/app/elements/change/gr-comment-list/gr-comment-list.js
index 72f1bfe..7e7a0ec 100644
--- a/polygerrit-ui/app/elements/change/gr-comment-list/gr-comment-list.js
+++ b/polygerrit-ui/app/elements/change/gr-comment-list/gr-comment-list.js
@@ -13,10 +13,6 @@
// limitations under the License.
(function() {
'use strict';
-
- const COMMIT_MESSAGE_PATH = '/COMMIT_MSG';
- const MERGE_LIST_PATH = '/MERGE_LIST';
-
Polymer({
is: 'gr-comment-list',
@@ -46,15 +42,6 @@
file, patchNum);
},
- _computeFileDisplayName(path) {
- if (path === COMMIT_MESSAGE_PATH) {
- return 'Commit message';
- } else if (path === MERGE_LIST_PATH) {
- return 'Merge list';
- }
- return path;
- },
-
_isOnParent(comment) {
return comment.side === 'PARENT';
},
diff --git a/polygerrit-ui/app/elements/change/gr-comment-list/gr-comment-list_test.html b/polygerrit-ui/app/elements/change/gr-comment-list/gr-comment-list_test.html
index ed1ece6..0e47e30 100644
--- a/polygerrit-ui/app/elements/change/gr-comment-list/gr-comment-list_test.html
+++ b/polygerrit-ui/app/elements/change/gr-comment-list/gr-comment-list_test.html
@@ -60,15 +60,6 @@
assert.deepEqual(element._computeFilesFromComments(null), []);
});
- test('_computeFileDisplayName', () => {
- assert.equal(element._computeFileDisplayName('/COMMIT_MSG'),
- 'Commit message');
- assert.equal(element._computeFileDisplayName('/MERGE_LIST'),
- 'Merge list');
- assert.equal(element._computeFileDisplayName('/foo/bar/baz'),
- '/foo/bar/baz');
- });
-
test('_computePatchDisplayName', () => {
const comment = {line: 123, side: 'REVISION', patch_set: 10};
diff --git a/polygerrit-ui/app/elements/change/gr-file-list-header/gr-file-list-header.html b/polygerrit-ui/app/elements/change/gr-file-list-header/gr-file-list-header.html
index 252ea71..b9c1328 100644
--- a/polygerrit-ui/app/elements/change/gr-file-list-header/gr-file-list-header.html
+++ b/polygerrit-ui/app/elements/change/gr-file-list-header/gr-file-list-header.html
@@ -47,11 +47,13 @@
background-color: #f6f6f6;
border-bottom: 1px solid #ebebeb;
display: flex;
- padding: .5em calc(var(--default-horizontal-margin) / 2);
+ height: 3.2em;
+ padding: 0 calc(var(--default-horizontal-margin) / 2);
}
.patchInfo-header-wrapper {
align-items: center;
display: flex;
+ margin: 0 .25em;
width: 100%;
}
.latestPatchContainer {
@@ -81,7 +83,9 @@
align-items: center;
display: flex;
font-weight: bold;
- margin: .5em calc(var(--default-horizontal-margin) / 2) 0;
+ height: 2.25em;
+ margin: 0 calc(var(--default-horizontal-margin) / 2);
+ padding: 0 .25em;
}
.rightControls {
align-items: center;
@@ -102,20 +106,26 @@
.editLoaded .showOnEdit {
display: initial;
}
+ .label {
+ font-family: var(--font-family-bold);
+ margin-right: 1em;
+ }
@media screen and (max-width: 50em) {
.desktop {
display: none;
}
}
</style>
- <div class$="patchInfo-header [[_computeEditLoadedClass(editLoaded)]] [[_computePatchInfoClass(patchRange.patchNum, allPatchSets)]]">
+ <div class$="patchInfo-header [[_computeEditLoadedClass(editLoaded)]] [[_computePatchInfoClass(patchNum, allPatchSets)]]">
<div class="patchInfo-header-wrapper">
<div>
+ <span class="label">Files</span>
<gr-patch-range-select
id="rangeSelect"
comments="[[comments]]"
change-num="[[changeNum]]"
- patch-range="[[patchRange]]"
+ patch-num="{{patchNum}}"
+ base-patch-num="{{basePatchNum}}"
available-patches="[[allPatchSets]]"
revisions="[[change.revisions]]"
on-patch-range-change="_handlePatchChange">
@@ -141,7 +151,7 @@
id="descriptionLabel"
class="descriptionLabel"
label-text="Add patchset description"
- value="[[_computePatchSetDescription(change, patchRange.patchNum)]]"
+ value="[[_computePatchSetDescription(change, patchNum)]]"
placeholder="[[_computeDescriptionPlaceholder(_descriptionReadOnly)]]"
read-only="[[_descriptionReadOnly]]"
on-changed="_handleDescriptionChanged"></gr-editable-label>
diff --git a/polygerrit-ui/app/elements/change/gr-file-list-header/gr-file-list-header.js b/polygerrit-ui/app/elements/change/gr-file-list-header/gr-file-list-header.js
index e1c7977..4b6d59c 100644
--- a/polygerrit-ui/app/elements/change/gr-file-list-header/gr-file-list-header.js
+++ b/polygerrit-ui/app/elements/change/gr-file-list-header/gr-file-list-header.js
@@ -38,8 +38,16 @@
type: String,
notify: true,
},
- /** @type {?} */
- patchRange: Object,
+ patchNum: {
+ type: String,
+ notify: true,
+ observer: '_patchOrBaseChanged',
+ },
+ basePatchNum: {
+ type: String,
+ notify: true,
+ observer: '_patchOrBaseChanged',
+ },
revisions: Array,
// Caps the number of files that can be shown and have the 'show diffs' /
// 'hide diffs' buttons still be functional.
@@ -67,7 +75,7 @@
},
_computeDescriptionPlaceholder(readOnly) {
- return (readOnly ? 'No' : 'Add a') + ' patch set description';
+ return (readOnly ? 'No' : 'Add') + ' patchset description';
},
_computeDescriptionReadOnly(loggedIn, change, account) {
@@ -80,7 +88,6 @@
rev.description.substring(0, PATCH_DESC_MAX_LENGTH) : '';
},
-
/**
* @param {!Object} revisions The revisions object keyed by revision hashes
* @param {?Object} patchSet A revision already fetched from {revisions}
@@ -98,10 +105,10 @@
_handleDescriptionChanged(e) {
const desc = e.detail.trim();
const rev = this.getRevisionByPatchNum(this.change.revisions,
- this.patchRange.patchNum);
+ this.patchNum);
const sha = this._getPatchsetHash(this.change.revisions, rev);
this.$.restAPI.setDescription(this.changeNum,
- this.patchRange.patchNum, desc)
+ this.patchNum, desc)
.then(res => {
if (res.ok) {
this.set(['_change', 'revisions', sha, 'description'], desc);
@@ -137,35 +144,18 @@
this.findSortedIndex(basePatchNum, this.revisions);
},
- /**
- * Change active patch to the provided patch num.
- * @param {number|string} basePatchNum the base patch to be viewed.
- * @param {number|string} patchNum the patch number to be viewed.
- * @param {boolean} opt_forceParams When set to true, the resulting URL will
- * always include the patch range, even if the requested patchNum is
- * known to be the latest.
+ /*
+ * Triggered by _patchNum and _basePatchNum observer, in order to detect if
+ * the patch has been previously set or not. The new patch number is not
+ * explicitly used, because this could be called by either _patchNum or
+ * _basePatchNum's observer. Since the behavior is the same, they are
+ * combined.
*/
- _changePatchNum(basePatchNum, patchNum, opt_forceParams) {
- if (!opt_forceParams) {
- let currentPatchNum;
- if (this.change.current_revision) {
- currentPatchNum =
- this.change.revisions[this.change.current_revision]._number;
- } else {
- currentPatchNum = this.computeLatestPatchNum(this.allPatchSets);
- }
- if (this.patchNumEquals(patchNum, currentPatchNum) &&
- basePatchNum === 'PARENT') {
- Gerrit.Nav.navigateToChange(this.change);
- return;
- }
- }
- Gerrit.Nav.navigateToChange(this.change, patchNum,
- basePatchNum);
- },
+ _patchOrBaseChanged(patchNew, patchOld) {
+ if (!patchOld) { return; }
- _handlePatchChange(e) {
- this._changePatchNum(e.detail.leftPatch, e.detail.rightPatch, true);
+ Gerrit.Nav.navigateToChange(this.change, this.patchNum,
+ this.basePatchNum);
},
_handlePrefsTap(e) {
diff --git a/polygerrit-ui/app/elements/change/gr-file-list-header/gr-file-list-header_test.html b/polygerrit-ui/app/elements/change/gr-file-list-header/gr-file-list-header_test.html
index 62ae5c7..f2bf809 100644
--- a/polygerrit-ui/app/elements/change/gr-file-list-header/gr-file-list-header_test.html
+++ b/polygerrit-ui/app/elements/change/gr-file-list-header/gr-file-list-header_test.html
@@ -43,11 +43,9 @@
suite('gr-file-list-header tests', () => {
let element;
let sandbox;
- let navigateToChangeStub;
setup(() => {
sandbox = sinon.sandbox.create();
- navigateToChangeStub = sandbox.stub(Gerrit.Nav, 'navigateToChange');
stub('gr-rest-api-interface', {
getConfig() { return Promise.resolve({test: 'config'}); },
getAccount() { return Promise.resolve(null); },
@@ -93,9 +91,9 @@
test('_computeDescriptionPlaceholder', () => {
assert.equal(element._computeDescriptionPlaceholder(true),
- 'No patch set description');
+ 'No patchset description');
assert.equal(element._computeDescriptionPlaceholder(false),
- 'Add a patch set description');
+ 'Add patchset description');
});
test('_computePatchSetDisabled', () => {
@@ -129,10 +127,9 @@
sandbox.stub(element, '_computeDescriptionReadOnly');
element.changeNum = '42';
- element.patchRange = {
- basePatchNum: 'PARENT',
- patchNum: 1,
- };
+ element.basePatchNum = 'PARENT';
+ element.patchNum = 1;
+
element.change = {
change_id: 'Iad9dc96274af6946f3632be53b106ef80f7ba6ca',
revisions: {
@@ -180,10 +177,8 @@
const computeSpy = sandbox.spy(element, '_fileListActionsVisible');
element._files = [];
element.changeNum = '42';
- element.patchRange = {
- basePatchNum: 'PARENT',
- patchNum: '2',
- };
+ element.basePatchNum = 'PARENT';
+ element.patchNum = '2';
element.shownFileCount = 1;
flush(() => {
assert.isTrue(computeSpy.lastCall.returnValue);
@@ -206,21 +201,8 @@
assert.equal(select.nativeSelect.value, 'UNIFIED_DIFF');
});
- test('_changePatchNum called when range select changes', () => {
- const leftPatch = 1;
- const rightPatch = 2;
- sandbox.stub(element, '_changePatchNum');
- element.$.rangeSelect.fire('patch-range-change', {leftPatch, rightPatch});
- assert.isTrue(element._changePatchNum.lastCall
- .calledWithExactly(1, 2, true));
- });
-
- test('include base patch when not parent', () => {
- element.changeNum = '42';
- element.patchRange = {
- basePatchNum: '2',
- patchNum: '3',
- };
+ test('navigateToChange called when range select changes', () => {
+ const navigateToChangeStub = sandbox.stub(Gerrit.Nav, 'navigateToChange');
element.change = {
change_id: 'Iad9dc96274af6946f3632be53b106ef80f7ba6ca',
revisions: {
@@ -232,16 +214,12 @@
status: 'NEW',
labels: {},
};
-
- element._changePatchNum(2, 13);
- assert.isTrue(navigateToChangeStub.lastCall.calledWithExactly(
- element.change, 13, 2));
-
- element.patchRange.basePatchNum = 'PARENT';
-
- element._changePatchNum('PARENT', 3);
- assert.isTrue(navigateToChangeStub.lastCall.calledWithExactly(
- element.change, 3, 'PARENT'));
+ element.basePatchNum = 1;
+ element.patchNum = 2;
+ element.patchNum = 3;
+ assert.equal(navigateToChangeStub.callCount, 1);
+ assert.isTrue(navigateToChangeStub.lastCall
+ .calledWithExactly(element.change, 3, 1));
});
test('class is applied to file list on old patch set', () => {
diff --git a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.html b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.html
index 10f4b27..c450fee 100644
--- a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.html
+++ b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.html
@@ -14,9 +14,10 @@
limitations under the License.
-->
+<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="../../../behaviors/async-foreach-behavior/async-foreach-behavior.html">
<link rel="import" href="../../../behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.html">
<link rel="import" href="../../../behaviors/gr-patch-set-behavior/gr-patch-set-behavior.html">
-<link rel="import" href="../../../bower_components/polymer/polymer.html">
<link rel="import" href="../../core/gr-navigation/gr-navigation.html">
<link rel="import" href="../../core/gr-reporting/gr-reporting.html">
<link rel="import" href="../../diff/gr-comment-api/gr-comment-api.html">
@@ -40,7 +41,8 @@
align-items: center;
border-top: 1px solid #eee;
display: flex;
- padding: .1em .25em;
+ height: 2.25em;
+ padding: 0 .25em;
}
:host(.loading) .row {
opacity: .5;
@@ -127,7 +129,7 @@
display: none;
}
label.show-hide {
- color: #00f;
+ color: var(--color-link);
cursor: pointer;
display: block;
font-size: .8em;
@@ -240,13 +242,13 @@
data-url="[[_computeDiffURL(change, patchRange.patchNum, patchRange.basePatchNum, file.__path)]]"
class$="[[_computePathClass(file.__path, _expandedFilePaths.*)]]">
<a href$="[[_computeDiffURL(change, patchRange.patchNum, patchRange.basePatchNum, file.__path)]]">
- <span title$="[[_computeFileDisplayName(file.__path)]]"
+ <span title$="[[computeDisplayPath(file.__path)]]"
class="fullFileName">
- [[_computeFileDisplayName(file.__path)]]
+ [[computeDisplayPath(file.__path)]]
</span>
- <span title$="[[_computeFileDisplayName(file.__path)]]"
+ <span title$="[[computeDisplayPath(file.__path)]]"
class="truncatedFileName">
- [[_computeTruncatedFileDisplayName(file.__path)]]
+ [[computeTruncatedPath(file.__path)]]
</span>
</a>
<div class="oldPath" hidden$="[[!file.old_path]]" hidden
diff --git a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.js b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.js
index 7807f5e..68761fa 100644
--- a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.js
+++ b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.js
@@ -19,8 +19,6 @@
// Maximum length for patch set descriptions.
const PATCH_DESC_MAX_LENGTH = 500;
const WARN_SHOW_ALL_THRESHOLD = 1000;
- const COMMIT_MESSAGE_PATH = '/COMMIT_MSG';
- const MERGE_LIST_PATH = '/MERGE_LIST';
const LOADING_DEBOUNCE_INTERVAL = 100;
const FileStatus = {
@@ -120,8 +118,10 @@
},
behaviors: [
+ Gerrit.AsyncForeachBehavior,
Gerrit.KeyboardShortcutBehavior,
Gerrit.PatchSetBehavior,
+ Gerrit.PathListBehavior,
],
observers: [
@@ -665,19 +665,6 @@
return Gerrit.Nav.getUrlForDiff(change, path, patchNum, basePatchNum);
},
- _computeFileDisplayName(path) {
- if (path === COMMIT_MESSAGE_PATH) {
- return 'Commit message';
- } else if (path === MERGE_LIST_PATH) {
- return 'Merge list';
- }
- return path;
- },
-
- _computeTruncatedFileDisplayName(path) {
- return util.truncatePath(this._computeFileDisplayName(path));
- },
-
_formatBytes(bytes) {
if (bytes == 0) return '+/-0 B';
const bits = 1024;
@@ -706,7 +693,7 @@
_computeClass(baseClass, path) {
const classes = [baseClass];
- if (path === COMMIT_MESSAGE_PATH || path === MERGE_LIST_PATH) {
+ if (path === this.COMMIT_MESSAGE_PATH || path === this.MERGE_LIST_PATH) {
classes.push('invisible');
}
return classes.join(' ');
@@ -858,22 +845,20 @@
* @return {!Promise}
*/
_renderInOrder(paths, diffElements, initialCount) {
- if (!paths.length) {
+ let iter = 0;
+ return this.asyncForeach(paths, path => {
+ iter++;
+ console.log('Expanding diff', iter, 'of', initialCount, ':', path);
+ const diffElem = this._findDiffByPath(path, diffElements);
+ diffElem.comments = this.$.commentAPI.getCommentsForPath(path,
+ this.patchRange, this.projectConfig);
+ const promises = [diffElem.reload()];
+ if (this._isLoggedIn) {
+ promises.push(this._reviewFile(path));
+ }
+ return Promise.all(promises);
+ }).then(() => {
console.log('Finished expanding', initialCount, 'diff(s)');
- return Promise.resolve();
- }
- console.log('Expanding diff', 1 + initialCount - paths.length, 'of',
- initialCount, ':', paths[0]);
- const diffElem = this._findDiffByPath(paths[0], diffElements);
- diffElem.comments = this.$.commentAPI.getCommentsForPath(paths[0],
- this.patchRange, this.projectConfig);
-
- const promises = [diffElem.reload()];
- if (this._isLoggedIn) {
- promises.push(this._reviewFile(paths[0]));
- }
- return Promise.all(promises).then(() => {
- return this._renderInOrder(paths.slice(1), diffElements, initialCount);
});
},
diff --git a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_test.html b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_test.html
index e77ad2c..b80a20f 100644
--- a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_test.html
+++ b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_test.html
@@ -577,11 +577,6 @@
assert.equal(element._computeFileStatus(undefined), 'M');
assert.equal(element._computeFileStatus(null), 'M');
- assert.equal(element._computeFileDisplayName('/foo/bar/baz'),
- '/foo/bar/baz');
- assert.equal(element._computeFileDisplayName('/COMMIT_MSG'),
- 'Commit message');
-
assert.equal(element._computeClass('clazz', '/foo/bar/baz'), 'clazz');
assert.equal(element._computeClass('clazz', '/COMMIT_MSG'),
'clazz invisible');
diff --git a/polygerrit-ui/app/elements/change/gr-label-score-row/gr-label-score-row.html b/polygerrit-ui/app/elements/change/gr-label-score-row/gr-label-score-row.html
index 36e2bc7..6496091 100644
--- a/polygerrit-ui/app/elements/change/gr-label-score-row/gr-label-score-row.html
+++ b/polygerrit-ui/app/elements/change/gr-label-score-row/gr-label-score-row.html
@@ -51,19 +51,24 @@
.selectedValueText.hidden {
display: none;
}
- iron-selector > gr-button:first-of-type {
- border-bottom-left-radius: 2px;
- border-top-left-radius: 2px;
- }
- iron-selector > gr-button:last-of-type {
- border-bottom-right-radius: 2px;
- border-top-right-radius: 2px;
- }
- iron-selector > gr-button.iron-selected {
- background-color: #ddd;
- }
gr-button {
min-width: 40px;
+ --gr-button: {
+ border: 1px solid #d1d2d3;
+ border-radius: 12px;
+ box-shadow: none;
+ padding: .2em .85em;
+ }
+ --gr-button-background: #f5f5f5;
+ --gr-button-color: black;
+ --gr-button-hover-color: black;
+
+ }
+ iron-selector > gr-button.iron-selected {
+ --gr-button-background:#ddd;
+ --gr-button-color: black;
+ --gr-button-hover-background-color: #ddd;
+ --gr-button-hover-color: black;
}
.placeholder {
display: inline-block;
diff --git a/polygerrit-ui/app/elements/change/gr-label-score-row/gr-label-score-row.js b/polygerrit-ui/app/elements/change/gr-label-score-row/gr-label-score-row.js
index 812f0bd..dd0bccc 100644
--- a/polygerrit-ui/app/elements/change/gr-label-score-row/gr-label-score-row.js
+++ b/polygerrit-ui/app/elements/change/gr-label-score-row/gr-label-score-row.js
@@ -106,6 +106,8 @@
// nothing and then to the new item.
if (!e.target.selectedItem) { return; }
this._selectedValueText = e.target.selectedItem.getAttribute('title');
+ // Needed to update the style of the selected button.
+ this.updateStyles();
this.fire('labels-changed');
},
diff --git a/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list_test.html b/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list_test.html
index c47d63f..15964a7 100644
--- a/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list_test.html
+++ b/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list_test.html
@@ -86,14 +86,14 @@
assert.isFalse(element.$.messageControlsContainer.hasAttribute('hidden'));
assert.equal(getMessages().length, 20);
- assert.equal(element.$.incrementMessagesBtn.innerText.trim(),
- 'Show 5 more');
+ assert.equal(element.$.incrementMessagesBtn.innerText.toUpperCase()
+ .trim(), 'SHOW 5 MORE');
MockInteractions.tap(element.$.incrementMessagesBtn);
flushAsynchronousOperations();
assert.equal(getMessages().length, 25);
- assert.equal(element.$.incrementMessagesBtn.innerText.trim(),
- 'Show 1 more');
+ assert.equal(element.$.incrementMessagesBtn.innerText.toUpperCase()
+ .trim(), 'SHOW 1 MORE');
MockInteractions.tap(element.$.incrementMessagesBtn);
flushAsynchronousOperations();
@@ -108,7 +108,8 @@
assert.isFalse(element.$.messageControlsContainer.hasAttribute('hidden'));
assert.equal(getMessages().length, 20);
- assert.equal(element.$.oldMessagesBtn.innerText, 'Show all 6 messages');
+ assert.equal(element.$.oldMessagesBtn.innerText.toUpperCase(),
+ 'SHOW ALL 6 MESSAGES');
MockInteractions.tap(element.$.oldMessagesBtn);
flushAsynchronousOperations();
@@ -121,7 +122,8 @@
.concat(_.times(11, randomMessage));
flushAsynchronousOperations();
- assert.equal(element.$.oldMessagesBtn.innerText, 'Show 1 message');
+ assert.equal(element.$.oldMessagesBtn.innerText.toUpperCase(),
+ 'SHOW 1 MESSAGE');
assert.isFalse(element.$.messageControlsContainer.hasAttribute('hidden'));
MockInteractions.tap(element.$.automatedMessageToggle);
flushAsynchronousOperations();
@@ -134,12 +136,14 @@
.concat(_.times(11, randomAutomated));
flushAsynchronousOperations();
- assert.equal(element.$.oldMessagesBtn.innerText, 'Show 1 message');
+ assert.equal(element.$.oldMessagesBtn.innerText.toUpperCase(),
+ 'SHOW 1 MESSAGE');
assert.isFalse(element.$.messageControlsContainer.hasAttribute('hidden'));
MockInteractions.tap(element.$.automatedMessageToggle);
flushAsynchronousOperations();
- assert.equal(element.$.oldMessagesBtn.innerText, 'Show 1 message');
+ assert.equal(element.$.oldMessagesBtn.innerText.toUpperCase(),
+ 'SHOW 1 MESSAGE');
assert.isFalse(element.$.messageControlsContainer.hasAttribute('hidden'));
});
diff --git a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.html b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.html
index 161dfe7..4b02c3d 100644
--- a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.html
+++ b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.html
@@ -117,10 +117,6 @@
.draftsContainer h3 {
margin-top: .25em;
}
- .action:link,
- .action:visited {
- color: #00e;
- }
#checkingStatusLabel,
#notLatestLabel {
margin-left: 1em;
diff --git a/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header.html b/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header.html
index 09f3029..9f5d39d 100644
--- a/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header.html
+++ b/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header.html
@@ -67,7 +67,9 @@
.linksTitle {
color: var(--primary-text-color);
display: inline-block;
+ font-family: var(--font-family-bold);
position: relative;
+ text-transform: uppercase;
}
.linksTitle:hover {
opacity: .75;
@@ -157,7 +159,9 @@
</li>
</template>
<li>
- <a class="browse linksTitle" href$="[[_computeRelativeURL('/admin/projects')]]">
+ <a
+ class="browse linksTitle"
+ href$="[[_computeRelativeURL('/admin/projects')]]">
Browse</a>
</li>
</ul>
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.html b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.html
index 008852e..203d34f 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.html
@@ -16,6 +16,7 @@
<link rel="import" href="../../../bower_components/polymer/polymer.html">
<link rel="import" href="../../../behaviors/gr-patch-set-behavior/gr-patch-set-behavior.html">
+<link rel="import" href="../../../behaviors/gr-path-list-behavior/gr-path-list-behavior.html">
<link rel="import" href="../../../behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.html">
<link rel="import" href="../../../behaviors/rest-client-behavior/rest-client-behavior.html">
<link rel="import" href="../../../bower_components/iron-dropdown/iron-dropdown.html">
@@ -85,7 +86,6 @@
vertical-align: middle;
}
.dropdown-trigger {
- color: #00e;
cursor: pointer;
padding: 0;
}
@@ -111,7 +111,7 @@
width: .3em;
}
.dropdown-content a:hover {
- background-color: #00e;
+ background-color: var(--color-link);
color: #fff;
}
.dropdown-content a[selected] {
@@ -125,7 +125,6 @@
color: #000;
}
gr-button {
- font: inherit;
padding: .3em 0;
text-decoration: none;
}
@@ -229,7 +228,7 @@
hidden$="[[!_loggedIn]]" hidden>
<div class="jumpToFileContainer desktop">
<gr-button link class="dropdown-trigger" id="trigger" on-tap="_showDropdownTapHandler">
- <span>[[_computeFileDisplayName(_path)]]</span>
+ <span>[[computeDisplayPath(_path)]]</span>
<span class="downArrow">▼</span>
</gr-button>
<!-- *-align="" to disable iron-dropdown's element positioning. -->
@@ -246,7 +245,7 @@
<a href$="[[_computeDiffURL(_change, _patchRange.*, path)]]"
selected$="[[_computeFileSelected(path, _path)]]"
data-key-nav$="[[_computeKeyNav(path, _path, _fileList)]]"
- on-tap="_handleFileTap">[[_computeFileDisplayName(path)]]</a>
+ on-tap="_handleFileTap">[[computeDisplayPath(path)]]</a>
</template>
</div>
</iron-dropdown>
@@ -257,7 +256,7 @@
<option
value$="[[path]]"
selected$="[[_computeFileSelected(path, _path)]]">
- [[_computeTruncatedFileDisplayName(path)]]
+ [[computeTruncatedPath(path)]]
</option>
</template>
</select>
@@ -282,11 +281,11 @@
<gr-patch-range-select
id="rangeSelect"
change-num="[[_changeNum]]"
- patch-range="[[_patchRange]]"
+ patch-num="{{_patchNum}}"
+ base-patch-num="{{_basePatchNum}}"
files-weblinks="[[_filesWeblinks]]"
- available-patches="[[_computeAvailablePatches(_change.revisions, _change.revisions.*)]]"
- revisions="[[_change.revisions]]"
- on-patch-range-change="_handlePatchChange">
+ available-patches="[[_allPatchSets]]"
+ revisions="[[_change.revisions]]">
</gr-patch-range-select>
<span class="download desktop">
<span class="separator">/</span>
@@ -331,7 +330,7 @@
<a class="mobileNavLink"
href$="[[_computeNavLinkURL(_change, _path, _fileList, -1, 1)]]">
<</a>
- <div class="fullFileName mobile">[[_computeFileDisplayName(_path)]]
+ <div class="fullFileName mobile">[[computeDisplayPath(_path)]]
</div>
<a class="mobileNavLink"
href$="[[_computeNavLinkURL(_change, _path, _fileList, 1, 1)]]">
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.js b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.js
index fbd845a..c4746f2 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.js
@@ -14,9 +14,6 @@
(function() {
'use strict';
- const COMMIT_MESSAGE_PATH = '/COMMIT_MSG';
- const MERGE_LIST_PATH = '/MERGE_LIST';
-
const ERR_REVIEW_STATUS = 'Couldn’t change file review status.';
const MSG_LOADING_BLAME = 'Loading blame...';
const MSG_LOADED_BLAME = 'Blame loaded';
@@ -66,6 +63,22 @@
},
_patchRange: Object,
+ // These are kept as separate properties from the patchRange so that the
+ // observer can be aware of the previous value. In order to view sub
+ // property changes for _patchRange, a complex observer must be used, and
+ // that only displays the new value.
+ //
+ // If a previous value did not exist, the change is not reloaded with the
+ // new patches. This is just the initial setting from the change view vs.
+ // an update coming from the two way data binding.
+ _patchNum: {
+ type: String,
+ observer: '_patchOrBaseChanged',
+ },
+ _basePatchNum: {
+ type: String,
+ observer: '_patchOrBaseChanged',
+ },
/**
* @type {{
* subject: string,
@@ -127,7 +140,6 @@
type: Boolean,
computed: '_computeEditLoaded(_patchRange.*)',
},
-
_isBlameSupported: {
type: Boolean,
value: false,
@@ -137,11 +149,16 @@
type: Boolean,
value: false,
},
+ _allPatchSets: {
+ type: Array,
+ computed: 'computeAllPatchSets(_change, _change.revisions.*)',
+ },
},
behaviors: [
Gerrit.KeyboardShortcutBehavior,
Gerrit.PatchSetBehavior,
+ Gerrit.PathListBehavior,
Gerrit.RESTClientBehavior,
],
@@ -149,6 +166,7 @@
'_getProjectConfig(_change.project)',
'_getFiles(_changeNum, _patchRange.*)',
'_setReviewedObserver(_loggedIn, params.*)',
+ '_patchRangeChanged(_patchRange.*)',
],
keyBindings: {
@@ -495,7 +513,7 @@
// has been queued, the event can bubble up to the handler in gr-app.
this.async(() => {
this.fire('title-change',
- {title: this._computeTruncatedFileDisplayName(this._path)});
+ {title: this.computeTruncatedPath(this._path)});
});
// When navigating away from the page, there is a possibility that the
@@ -565,10 +583,21 @@
this.$.cursor.initialLineNumber = params.lineNum;
},
+ _patchRangeChanged() {
+ this._basePatchNum = this._patchRange.basePatchNum;
+ this._patchNum = this._patchRange.patchNum;
+ },
+
+ _patchOrBaseChanged(patchNew, patchOld) {
+ if (!patchOld) { return; }
+
+ this._handlePatchChange(this._basePatchNum, this._patchNum);
+ },
+
_pathChanged(path) {
if (path) {
this.fire('title-change',
- {title: this._computeTruncatedFileDisplayName(path)});
+ {title: this.computeTruncatedPath(path)});
}
if (this._fileList.length == 0) { return; }
@@ -595,12 +624,6 @@
return patchStr;
},
- _computeAvailablePatches(revs) {
- return this.sortRevisions(Object.values(revs)).map(e => {
- return {num: e._number};
- });
- },
-
/**
* When the latest patch of the change is selected (and there is no base
* patch) then the patch range need not appear in the URL. Return a patch
@@ -640,19 +663,6 @@
return this._getChangePath(change, patchRangeRecord.base, revisions);
},
- _computeFileDisplayName(path) {
- if (path === COMMIT_MESSAGE_PATH) {
- return 'Commit message';
- } else if (path === MERGE_LIST_PATH) {
- return 'Merge list';
- }
- return path;
- },
-
- _computeTruncatedFileDisplayName(path) {
- return util.truncatePath(this._computeFileDisplayName(path));
- },
-
_computeFileSelected(path, currentPath) {
return path == currentPath;
},
@@ -690,11 +700,9 @@
this.$.dropdown.open();
},
- _handlePatchChange(e) {
- const rightPatch = e.detail.rightPatch;
- const leftPatch = e.detail.leftPatch;
+ _handlePatchChange(basePatchNum, patchNum) {
Gerrit.Nav.navigateToDiff(
- this._change, this._path, rightPatch, leftPatch);
+ this._change, this._path, patchNum, basePatchNum);
},
_handlePrefsTap(e) {
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view_test.html b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view_test.html
index 68c2e52..8587fe5 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view_test.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view_test.html
@@ -163,6 +163,7 @@
_number: 42,
revisions: {
a: {_number: 10},
+ b: {_number: 5},
},
};
element._fileList = ['chell.go', 'glados.txt', 'wheatley.md'];
@@ -336,15 +337,6 @@
'42-glados.txt-10-PARENT');
assert.equal(linkEls[2].getAttribute('href'),
'42-wheatley.md-10-PARENT');
-
- assert.equal(element._computeFileDisplayName('/foo/bar/baz'),
- '/foo/bar/baz');
- assert.equal(element._computeFileDisplayName('/foobarbaz'),
- '/foobarbaz');
- assert.equal(element._computeFileDisplayName('/COMMIT_MSG'),
- 'Commit message');
- assert.equal(element._computeFileDisplayName('/MERGE_LIST'),
- 'Merge list');
});
test('jump to file dropdown with patch range', () => {
@@ -447,15 +439,23 @@
});
test('_handlePatchChange calls navigateToDiff correctly', () => {
- const leftPatch = 'PARENT';
- const rightPatch = '3';
const navigateStub = sandbox.stub(Gerrit.Nav, 'navigateToDiff');
element._change = {_number: 321, project: 'foo/bar'};
element._path = 'path/to/file.txt';
- element.$.rangeSelect.fire('patch-range-change', {leftPatch, rightPatch});
+
+ element._patchRange = {
+ basePatchNum: 'PARENT',
+ patchNum: '3',
+ };
+
+ assert.equal(element._basePatchNum, element._patchRange.basePatchNum);
+ assert.equal(element._patchNum, element._patchRange.patchNum);
+
+ element._patchNum = '1';
+
assert(navigateStub.lastCall.calledWithExactly(element._change,
- element._path, rightPatch, leftPatch));
+ element._path, '1', 'PARENT'));
});
test('download link', () => {
@@ -614,25 +614,6 @@
assert.equal(element.$.cursor.side, 'right');
});
- test('_shortenPath with long path should add ellipsis', () => {
- let path = 'level1/level2/level3/level4/file.js';
- let shortenedPath = util.truncatePath(path);
- // The expected path is truncated with an ellipsis.
- const expectedPath = '\u2026/file.js';
- assert.equal(shortenedPath, expectedPath);
-
- path = 'level2/file.js';
- shortenedPath = util.truncatePath(path);
- assert.equal(shortenedPath, expectedPath);
- });
-
- test('_shortenPath with short path should not add ellipsis', () => {
- const path = 'file.js';
- const expectedPath = 'file.js';
- const shortenedPath = util.truncatePath(path);
- assert.equal(shortenedPath, expectedPath);
- });
-
test('_onLineSelected', () => {
const getUrlStub = sandbox.stub(Gerrit.Nav, 'getUrlForDiffById');
const replaceStateStub = sandbox.stub(history, 'replaceState');
@@ -787,6 +768,7 @@
};
test('reviewed checkbox', () => {
+ sandbox.stub(element, '_handlePatchChange');
element._patchRange = {patchNum: '1'};
// Reviewed checkbox should be shown.
assert.isTrue(isVisible(element.$.reviewed));
diff --git a/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select.html b/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select.html
index a228a83..4f90502 100644
--- a/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select.html
+++ b/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select.html
@@ -13,11 +13,13 @@
See the License for the specific language governing permissions and
limitations under the License.
-->
+<link rel="import" href="../../../bower_components/polymer/polymer.html">
<link rel="import" href="../../../behaviors/gr-patch-set-behavior/gr-patch-set-behavior.html">
-<link rel="import" href="../../../bower_components/polymer/polymer.html">
-<link rel="import" href="../../shared/gr-select/gr-select.html">
<link rel="import" href="../../../styles/shared-styles.html">
+<link rel="import" href="../../shared/gr-dropdown-list/gr-dropdown-list.html">
+
+<link rel="import" href="../../shared/gr-select/gr-select.html">
<dom-module id="gr-patch-range-select">
<template>
@@ -32,27 +34,14 @@
.filesWeblinks {
display: none;
}
- select {
- max-width: 5.25em;
- }
}
</style>
- Patch set:
<span class="patchRange">
- <gr-select id="leftPatchSelect" bind-value="{{_leftSelected}}"
- on-change="_handlePatchChange">
- <select>
- <option value="PARENT">Base</option>
- <template is="dom-repeat" items="{{availablePatches}}" as="basePatchNum">
- <option value$="[[basePatchNum.num]]"
- disabled$="[[_computeLeftDisabled(basePatchNum.num, patchRange.patchNum, _sortedRevisions)]]">
- [[basePatchNum.num]]
- [[_computePatchSetCommentsString(comments, basePatchNum.num)]]
- [[_computePatchSetDescription(revisions, basePatchNum.num)]]
- </option>
- </template>
- </select>
- </gr-select>
+ <gr-dropdown-list
+ id="basePatchDropdown"
+ value="{{basePatchNum}}"
+ items="[[_baseDropdownContent]]">
+ </gr-dropdown-list>
</span>
<span is="dom-if" if="[[filesWeblinks.meta_a]]" class="filesWeblinks">
<template is="dom-repeat" items="[[filesWeblinks.meta_a]]" as="weblink">
@@ -62,19 +51,11 @@
</span>
→
<span class="patchRange">
- <gr-select id="rightPatchSelect" bind-value="{{_rightSelected}}"
- on-change="_handlePatchChange">
- <select>
- <template is="dom-repeat" items="{{availablePatches}}" as="patchNum">
- <option value$="[[patchNum.num]]"
- disabled$="[[_computeRightDisabled(patchNum.num, patchRange.basePatchNum, _sortedRevisions)]]">
- [[patchNum.num]]
- [[_computePatchSetCommentsString(comments, patchNum.num)]]
- [[_computePatchSetDescription(revisions, patchNum.num)]]
- </option>
- </template>
- </select>
- </gr-select>
+ <gr-dropdown-list
+ id="patchNumDropdown"
+ value="{{patchNum}}"
+ items="[[_patchDropdownContent]]">
+ </gr-dropdown-list>
<span is="dom-if" if="[[filesWeblinks.meta_b]]" class="filesWeblinks">
<template is="dom-repeat" items="[[filesWeblinks.meta_b]]" as="weblink">
<a target="_blank"
diff --git a/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select.js b/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select.js
index f6b759e..4ca9fb9 100644
--- a/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select.js
+++ b/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select.js
@@ -31,28 +31,89 @@
properties: {
availablePatches: Array,
+ _baseDropdownContent: {
+ type: Object,
+ computed: '_computeBaseDropdownContent(availablePatches, patchNum,' +
+ '_sortedRevisions, revisions)',
+ },
+ _patchDropdownContent: {
+ type: Object,
+ computed: '_computePatchDropdownContent(availablePatches,' +
+ 'basePatchNum, _sortedRevisions, revisions)',
+ },
changeNum: String,
comments: Array,
/** @type {{ meta_a: !Array, meta_b: !Array}} */
filesWeblinks: Object,
- /** @type {?} */
- patchRange: Object,
+ patchNum: {
+ type: String,
+ notify: true,
+ },
+ basePatchNum: {
+ type: String,
+ notify: true,
+ },
revisions: Object,
_sortedRevisions: Array,
- _rightSelected: String,
- _leftSelected: String,
},
observers: [
'_updateSortedRevisions(revisions.*)',
- '_updateSelected(patchRange.*)',
],
behaviors: [Gerrit.PatchSetBehavior],
- _updateSelected() {
- this._rightSelected = this.patchRange.patchNum;
- this._leftSelected = this.patchRange.basePatchNum;
+ _computeBaseDropdownContent(availablePatches, patchNum, _sortedRevisions,
+ revisions) {
+ const dropdownContent = [];
+ dropdownContent.push({
+ text: 'Base',
+ value: 'PARENT',
+ });
+ for (const basePatch of availablePatches) {
+ const basePatchNum = basePatch.num;
+ dropdownContent.push({
+ disabled: this._computeLeftDisabled(
+ basePatch.num, patchNum, _sortedRevisions),
+ triggerText: `Patchset ${basePatchNum}`,
+ text: `Patchset ${basePatchNum}` +
+ this._computePatchSetCommentsString(this.comments, basePatchNum),
+ mobileText: this._computeMobileText(basePatchNum, this.comments,
+ revisions),
+ bottomText: `${this._computePatchSetDescription(
+ revisions, basePatchNum)}`,
+ value: basePatch.num,
+ });
+ }
+ return dropdownContent;
+ },
+
+ _computeMobileText(patchNum, comments, revisions) {
+ return `${patchNum}` +
+ `${this._computePatchSetCommentsString(this.comments, patchNum)}` +
+ `${this._computePatchSetDescription(revisions, patchNum, true)}`;
+ },
+
+ _computePatchDropdownContent(availablePatches, basePatchNum,
+ _sortedRevisions, revisions) {
+ const dropdownContent = [];
+ for (const patch of availablePatches) {
+ const patchNum = patch.num;
+ dropdownContent.push({
+ disabled: this._computeRightDisabled(patchNum, basePatchNum,
+ _sortedRevisions),
+ triggerText: `Patchset ${patchNum}`,
+ text: `Patchset ${patchNum}` +
+ `${this._computePatchSetCommentsString(
+ this.comments, patchNum)}`,
+ mobileText: this._computeMobileText(patchNum, this.comments,
+ revisions),
+ bottomText: `${this._computePatchSetDescription(
+ revisions, patchNum)}`,
+ value: patchNum,
+ });
+ }
+ return dropdownContent;
},
_updateSortedRevisions(revisionsRecord) {
@@ -60,13 +121,6 @@
this._sortedRevisions = this.sortRevisions(Object.values(revisions));
},
- _handlePatchChange(e) {
- const leftPatch = this._leftSelected;
- const rightPatch = this._rightSelected;
- this.fire('patch-range-change', {rightPatch, leftPatch});
- e.target.blur();
- },
-
_computeLeftDisabled(basePatchNum, patchNum, sortedRevisions) {
return this.findSortedIndex(basePatchNum, sortedRevisions) >=
this.findSortedIndex(patchNum, sortedRevisions);
@@ -85,11 +139,11 @@
// debounce these, but because they are detecting two different
// events, sometimes the timing was off and one ended up missing.
_synchronizeSelectionRight() {
- this.$.rightPatchSelect.value = this._rightSelected;
+ this.$.rightPatchSelect.value = this.patchNum;
},
_synchronizeSelectionLeft() {
- this.$.leftPatchSelect.value = this._leftSelected;
+ this.$.leftPatchSelect.value = this.basePatchNum;
},
// Copied from gr-file-list
@@ -145,7 +199,7 @@
}
let commentsStr = '';
if (numComments > 0) {
- commentsStr = '(' + numComments + ' comments';
+ commentsStr = ' (' + numComments + ' comments';
if (numUnresolved > 0) {
commentsStr += ', ' + numUnresolved + ' unresolved';
}
@@ -154,9 +208,15 @@
return commentsStr;
},
- _computePatchSetDescription(revisions, patchNum) {
+ /**
+ * @param {!Array} revisions
+ * @param {number|string} patchNum
+ * @param {boolean=} opt_addFrontSpace
+ */
+ _computePatchSetDescription(revisions, patchNum, opt_addFrontSpace) {
const rev = this.getRevisionByPatchNum(revisions, patchNum);
return (rev && rev.description) ?
+ (opt_addFrontSpace ? ' ' : '') +
rev.description.substring(0, PATCH_DESC_MAX_LENGTH) : '';
},
});
diff --git a/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select_test.html b/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select_test.html
index 92553a0..d49974b 100644
--- a/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select_test.html
+++ b/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select_test.html
@@ -79,19 +79,74 @@
patchRange.basePatchNum, sortedRevisions));
});
- test('_updateSelected called with subproperty changes', () => {
- sandbox.stub(element, '_updateSelected');
- element.patchRange = {patchNum: 1, basePatchNum: 'PARENT'};
- assert.equal(element._updateSelected.callCount, 1);
-
- element.set('patchRange.patchNum', 2);
- assert.equal(element._updateSelected.callCount, 2);
-
- element.set('patchRange.basePatchNum', 1);
- assert.equal(element._updateSelected.callCount, 3);
+ test('_computeBaseDropdownContent', () => {
+ element.comments = {};
+ const availablePatches = [
+ {num: 1},
+ {num: 2},
+ {num: 3},
+ {num: 'edit'},
+ ];
+ const revisions = [
+ {
+ commit: {},
+ _number: 2,
+ description: 'description',
+ },
+ {commit: {}},
+ {commit: {}},
+ {commit: {}},
+ ];
+ const patchNum = 1;
+ const sortedRevisions = [
+ {_number: 1},
+ {_number: 2},
+ {_number: element.EDIT_NAME, basePatchNum: 2},
+ {_number: 3},
+ ];
+ const expectedResult = [
+ {
+ text: 'Base',
+ value: 'PARENT',
+ },
+ {
+ disabled: true,
+ triggerText: 'Patchset 1',
+ text: 'Patchset 1',
+ mobileText: '1',
+ bottomText: '',
+ value: 1,
+ },
+ {
+ disabled: true,
+ triggerText: 'Patchset 2',
+ text: 'Patchset 2',
+ mobileText: '2 description',
+ bottomText: 'description',
+ value: 2,
+ },
+ {
+ disabled: true,
+ triggerText: 'Patchset 3',
+ text: 'Patchset 3',
+ mobileText: '3',
+ bottomText: '',
+ value: 3,
+ },
+ {
+ disabled: true,
+ triggerText: 'Patchset edit',
+ text: 'Patchset edit',
+ mobileText: 'edit',
+ bottomText: '',
+ value: 'edit',
+ },
+ ];
+ assert.deepEqual(element._computeBaseDropdownContent(availablePatches,
+ patchNum, sortedRevisions, revisions), expectedResult);
});
- test('_computeLeftDisabled called when patchNum updates', () => {
+ test('_computeBaseDropdownContent called when patchNum updates', () => {
element.revisions = [
{commit: {}},
{commit: {}},
@@ -104,118 +159,103 @@
{num: 3},
{num: 'edit'},
];
- element.patchRange = {patchNum: 2, basePatchNum: 'PARENT'};
+ element.patchNum = 2;
+ element.basePatchNum = 'PARENT';
+ flushAsynchronousOperations();
+
+ sandbox.stub(element, '_computeBaseDropdownContent');
+
+ // Should be recomputed for each available patch
+ element.set('patchNum', 1);
+ assert.equal(element._computeBaseDropdownContent.callCount, 1);
+ });
+
+ test('_computePatchDropdownContent called when basePatchNum updates', () => {
+ element.revisions = [
+ {commit: {}},
+ {commit: {}},
+ {commit: {}},
+ {commit: {}},
+ ];
+ element.availablePatches = [
+ {num: 1},
+ {num: 2},
+ {num: 3},
+ {num: 'edit'},
+ ];
+ element.patchNum = 2;
+ element.basePatchNum = 'PARENT';
flushAsynchronousOperations();
// Should be recomputed for each available patch
- sandbox.stub(element, '_computeLeftDisabled');
- element.set('patchRange.patchNum', '1');
- assert.equal(element._computeLeftDisabled.callCount, 4);
+ sandbox.stub(element, '_computePatchDropdownContent');
+ element.set('basePatchNum', 1);
+ assert.equal(element._computePatchDropdownContent.callCount, 1);
});
- test('_computeRightDisabled called when basePatchNum updates', () => {
- element.revisions = [
- {commit: {}},
- {commit: {}},
- {commit: {}},
- {commit: {}},
- ];
- element.availablePatches = [
+ test('_computePatchDropdownContent', () => {
+ element.comments = {};
+ const availablePatches = [
{num: 1},
{num: 2},
{num: 3},
{num: 'edit'},
];
- element.patchRange = {patchNum: 2, basePatchNum: 'PARENT'};
- flushAsynchronousOperations();
-
- // Should be recomputed for each available patch
- sandbox.stub(element, '_computeRightDisabled');
- element.set('patchRange.basePatchNum', '1');
- assert.equal(element._computeRightDisabled.callCount, 4);
- });
-
-
- test('changes in patch range fire event', done => {
- sandbox.stub(element, '_computeLeftDisabled').returns(false);
- sandbox.stub(element, '_computeRightDisabled').returns(false);
- const patchRangeChangedStub = sandbox.stub();
- element.addEventListener('patch-range-change', patchRangeChangedStub);
-
- const leftSelectEl = element.$.leftPatchSelect;
- const rightSelectEl = element.$.rightPatchSelect;
- const blurSpy = sandbox.spy(leftSelectEl, 'blur');
- element.changeNum = '42';
- element.path = 'path/to/file.txt';
- element.availablePatches =
- [{num: '1'}, {num: '2'}, {num: '3'}, {num: 'edit'}];
- element.patchRange = {
- basePatchNum: 'PARENT',
- patchNum: '3',
- };
- flushAsynchronousOperations();
-
- let numEvents = 0;
- leftSelectEl.addEventListener('change', e => {
- numEvents++;
- if (numEvents === 1) {
- assert.deepEqual(patchRangeChangedStub.lastCall.args[0].detail,
- {rightPatch: '3', leftPatch: 'PARENT'});
- leftSelectEl.nativeSelect.value = 'edit';
- element.fire('change', {}, {node: leftSelectEl});
- assert(blurSpy.called, 'Dropdown should be blurred after selection');
- } else if (numEvents === 2) {
- assert.deepEqual(patchRangeChangedStub.lastCall.args[0].detail,
- {rightPatch: '3', leftPatch: 'edit'});
- rightSelectEl.nativeSelect.value = '1';
- element.fire('change', {}, {node: rightSelectEl});
- }
- });
- rightSelectEl.addEventListener('change', e => {
- assert.deepEqual(patchRangeChangedStub.lastCall.args[0].detail,
- {rightPatch: '1', leftPatch: 'edit'});
- done();
- });
- leftSelectEl.nativeSelect.value = 'PARENT';
- rightSelectEl.nativeSelect.value = '3';
- element.fire('change', {}, {node: leftSelectEl});
- });
-
- test('diff against dropdown', done => {
- element.revisions = [
- {commit: {}},
+ const revisions = [
+ {
+ commit: {},
+ _number: 2,
+ description: 'description',
+ },
{commit: {}},
{commit: {}},
{commit: {}},
];
- element.availablePatches = [
- {num: 1},
- {num: 2},
- {num: 3},
- {num: 'edit'},
+ const basePatchNum = 1;
+ const sortedRevisions = [
+ {_number: 1},
+ {_number: 2},
+ {_number: element.EDIT_NAME, basePatchNum: 2},
+ {_number: 3},
];
- element.patchRange = {
- basePatchNum: 'PARENT',
- patchNum: '3',
- };
- const patchRangeChangedStub = sandbox.stub();
- element.addEventListener('patch-range-change', patchRangeChangedStub);
+ const expectedResult = [
+ {
+ disabled: true,
+ triggerText: 'Patchset 1',
+ text: 'Patchset 1',
+ mobileText: '1',
+ bottomText: '',
+ value: 1,
+ },
+ {
+ disabled: false,
+ triggerText: 'Patchset 2',
+ text: 'Patchset 2',
+ mobileText: '2 description',
+ bottomText: 'description',
+ value: 2,
+ },
+ {
+ disabled: false,
+ triggerText: 'Patchset 3',
+ text: 'Patchset 3',
+ mobileText: '3',
+ bottomText: '',
+ value: 3,
+ },
+ {
+ disabled: false,
+ triggerText: 'Patchset edit',
+ text: 'Patchset edit',
+ mobileText: 'edit',
+ bottomText: '',
+ value: 'edit',
+ },
+ ];
- flush(() => {
- const selectEl = element.$.leftPatchSelect;
- assert.equal(selectEl.nativeSelect.value, 'PARENT');
- assert.isTrue(element.$$('#leftPatchSelect option[value="3"]')
- .hasAttribute('disabled'));
- selectEl.addEventListener('change', () => {
- assert.equal(selectEl.nativeSelect.value, 'edit');
- assert.deepEqual(patchRangeChangedStub.lastCall.args[0].detail,
- {leftPatch: 'edit', rightPatch: '3'});
- done();
- });
- selectEl.nativeSelect.value = 'edit';
- element.fire('change', {}, {node: selectEl.nativeSelect});
- });
+ assert.deepEqual(element._computePatchDropdownContent(availablePatches,
+ basePatchNum, sortedRevisions, revisions), expectedResult);
});
test('filesWeblinks', () => {
@@ -264,12 +304,12 @@
};
assert.equal(element._computePatchSetCommentsString(comments, 1),
- '(3 comments, 1 unresolved)');
+ ' (3 comments, 1 unresolved)');
// Test string with no unresolved comments.
delete comments['foo'];
assert.equal(element._computePatchSetCommentsString(comments, 1),
- '(2 comments)');
+ ' (2 comments)');
// Test string with no comments.
delete comments['bar'];
diff --git a/polygerrit-ui/app/elements/diff/gr-syntax-themes/gr-theme-default.html b/polygerrit-ui/app/elements/diff/gr-syntax-themes/gr-theme-default.html
index 31a276f..311a5af 100644
--- a/polygerrit-ui/app/elements/diff/gr-syntax-themes/gr-theme-default.html
+++ b/polygerrit-ui/app/elements/diff/gr-syntax-themes/gr-theme-default.html
@@ -60,7 +60,7 @@
color: #219;
}
.gr-syntax-type {
- color: #00f;
+ color: var(--color-link);
}
.gr-syntax-title {
color: #0000C0;
diff --git a/polygerrit-ui/app/elements/plugins/gr-endpoint-decorator/gr-endpoint-decorator_test.html b/polygerrit-ui/app/elements/plugins/gr-endpoint-decorator/gr-endpoint-decorator_test.html
index 578989e..c7ab3d9 100644
--- a/polygerrit-ui/app/elements/plugins/gr-endpoint-decorator/gr-endpoint-decorator_test.html
+++ b/polygerrit-ui/app/elements/plugins/gr-endpoint-decorator/gr-endpoint-decorator_test.html
@@ -24,6 +24,8 @@
<link rel="import" href="gr-endpoint-decorator.html">
<link rel="import" href="../gr-endpoint-param/gr-endpoint-param.html">
+<script>void(0);</script>
+
<test-fixture id="basic">
<template>
<gr-endpoint-decorator name="foo">
diff --git a/polygerrit-ui/app/elements/plugins/gr-plugin-host/gr-plugin-host.js b/polygerrit-ui/app/elements/plugins/gr-plugin-host/gr-plugin-host.js
index 3a0c898..86c0961 100644
--- a/polygerrit-ui/app/elements/plugins/gr-plugin-host/gr-plugin-host.js
+++ b/polygerrit-ui/app/elements/plugins/gr-plugin-host/gr-plugin-host.js
@@ -47,12 +47,10 @@
* States that it expects no more than 3 parameters, but that's not true.
* @todo (beckysiegel) check Polymer annotations and submit change.
*/
-
_importHtmlPlugins(plugins) {
for (const url of plugins) {
this.importHref(
- this._urlFor(url), Gerrit._pluginInstalled, Gerrit._pluginInstalled,
- true);
+ this._urlFor(url), null, Gerrit._pluginInstalled, true);
}
},
diff --git a/polygerrit-ui/app/elements/plugins/gr-plugin-host/gr-plugin-host_test.html b/polygerrit-ui/app/elements/plugins/gr-plugin-host/gr-plugin-host_test.html
index 27adbe1..66c7511 100644
--- a/polygerrit-ui/app/elements/plugins/gr-plugin-host/gr-plugin-host_test.html
+++ b/polygerrit-ui/app/elements/plugins/gr-plugin-host/gr-plugin-host_test.html
@@ -23,6 +23,8 @@
<link rel="import" href="../../../test/common-test-setup.html"/>
<link rel="import" href="gr-plugin-host.html">
+<script>void(0);</script>
+
<test-fixture id="basic">
<template>
<gr-plugin-host></gr-plugin-host>
@@ -61,9 +63,9 @@
plugin: {html_resource_paths: ['foo/bar', 'baz']},
};
assert.isTrue(element.importHref.calledWith(
- '/foo/bar', Gerrit._pluginInstalled, Gerrit._pluginInstalled, true));
+ '/foo/bar', null, Gerrit._pluginInstalled, true));
assert.isTrue(element.importHref.calledWith(
- '/baz', Gerrit._pluginInstalled, Gerrit._pluginInstalled, true));
+ '/baz', null, Gerrit._pluginInstalled, true));
});
test('imports relative html plugins from config with a base url', () => {
@@ -71,11 +73,9 @@
element.config = {
plugin: {html_resource_paths: ['foo/bar', 'baz']}};
assert.isTrue(element.importHref.calledWith(
- '/the-base/foo/bar', Gerrit._pluginInstalled, Gerrit._pluginInstalled,
- true));
+ '/the-base/foo/bar', null, Gerrit._pluginInstalled, true));
assert.isTrue(element.importHref.calledWith(
- '/the-base/baz', Gerrit._pluginInstalled, Gerrit._pluginInstalled,
- true));
+ '/the-base/baz', null, Gerrit._pluginInstalled, true));
});
test('imports absolute html plugins from config', () => {
@@ -88,11 +88,9 @@
},
};
assert.isTrue(element.importHref.calledWith(
- 'http://example.com/foo/bar', Gerrit._pluginInstalled,
- Gerrit._pluginInstalled, true));
+ 'http://example.com/foo/bar', null, Gerrit._pluginInstalled, true));
assert.isTrue(element.importHref.calledWith(
- 'https://example.com/baz', Gerrit._pluginInstalled,
- Gerrit._pluginInstalled, true));
+ 'https://example.com/baz', null, Gerrit._pluginInstalled, true));
});
test('adds js plugins from config to the body', () => {
@@ -139,9 +137,9 @@
},
};
assert.isTrue(element.importHref.calledWith(
- '/oof', Gerrit._pluginInstalled, Gerrit._pluginInstalled, true));
+ '/oof', null, Gerrit._pluginInstalled, true));
assert.isTrue(element.importHref.calledWith(
- '/some', Gerrit._pluginInstalled, Gerrit._pluginInstalled, true));
+ '/some', null, Gerrit._pluginInstalled, true));
});
});
</script>
diff --git a/polygerrit-ui/app/elements/settings/gr-menu-editor/gr-menu-editor.html b/polygerrit-ui/app/elements/settings/gr-menu-editor/gr-menu-editor.html
index fa3428a..6805885 100644
--- a/polygerrit-ui/app/elements/settings/gr-menu-editor/gr-menu-editor.html
+++ b/polygerrit-ui/app/elements/settings/gr-menu-editor/gr-menu-editor.html
@@ -59,18 +59,21 @@
<td class="urlCell">[[item.url]]</td>
<td class="buttonColumn">
<gr-button
+ link
data-index="[[index]]"
on-tap="_handleMoveUpButton"
class="moveUpButton">↑</gr-button>
</td>
<td class="buttonColumn">
<gr-button
+ link
data-index="[[index]]"
on-tap="_handleMoveDownButton"
class="moveDownButton">↓</gr-button>
</td>
<td>
<gr-button
+ link
data-index="[[index]]"
on-tap="_handleDeleteButton"
class="remove-button">Delete</gr-button>
@@ -99,6 +102,7 @@
<th></th>
<th>
<gr-button
+ link
disabled$="[[_computeAddDisabled(_newName, _newUrl)]]"
on-tap="_handleAddButton">Add</gr-button>
</th>
diff --git a/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view.html b/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view.html
index cacccda..7764d3b 100644
--- a/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view.html
+++ b/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view.html
@@ -353,6 +353,7 @@
id="emailEditor"
has-unsaved-changes="{{_emailsChanged}}"></gr-email-editor>
<gr-button
+ link
on-tap="_handleSaveEmails"
disabled$="[[!_emailsChanged]]">Save changes</gr-button>
</fieldset>
diff --git a/polygerrit-ui/app/elements/settings/gr-watched-projects-editor/gr-watched-projects-editor.html b/polygerrit-ui/app/elements/settings/gr-watched-projects-editor/gr-watched-projects-editor.html
index a834ea2..68b2c23 100644
--- a/polygerrit-ui/app/elements/settings/gr-watched-projects-editor/gr-watched-projects-editor.html
+++ b/polygerrit-ui/app/elements/settings/gr-watched-projects-editor/gr-watched-projects-editor.html
@@ -83,6 +83,7 @@
</template>
<td>
<gr-button
+ link
data-index$="[[projectIndex]]"
on-tap="_handleRemoveProject">Delete</gr-button>
</td>
@@ -106,7 +107,7 @@
placeholder="branch:name, or other search expression">
</th>
<th>
- <gr-button on-tap="_handleAddProject">Add</gr-button>
+ <gr-button link on-tap="_handleAddProject">Add</gr-button>
</th>
</tr>
</tfoot>
diff --git a/polygerrit-ui/app/elements/shared/gr-account-chip/gr-account-chip.html b/polygerrit-ui/app/elements/shared/gr-account-chip/gr-account-chip.html
index 90594de..b658025 100644
--- a/polygerrit-ui/app/elements/shared/gr-account-chip/gr-account-chip.html
+++ b/polygerrit-ui/app/elements/shared/gr-account-chip/gr-account-chip.html
@@ -37,24 +37,31 @@
:host([show-avatar]) .container {
padding-left: 0;
}
- gr-button.remove,
gr-button.remove:hover,
gr-button.remove:focus {
- border-color: transparent;
- color: #333;
+ --gr-button: {
+ color: #333;
+ }
}
gr-button.remove {
- background: #eee;
- border: 0;
- color: #666;
- font-size: 1.7em;
- font-weight: normal;
- height: .6em;
- line-height: .6em;
- margin-left: .15em;
- margin-top: -.05em;
- padding: 0;
- text-decoration: none;
+ --gr-button: {
+ border: 0;
+ color: #666;
+ font-size: 1.7em;
+ font-weight: normal;
+ height: .6em;
+ line-height: .6em;
+ margin-left: .15em;
+ margin-top: -.05em;
+ padding: 0;
+ text-decoration: none;
+ }
+ --gr-button-hover-color: {
+ color: #333;
+ }
+ --gr-button-hover-background-color: {
+ color: #333;
+ }
}
:host:focus {
border-color: transparent;
@@ -78,6 +85,7 @@
<gr-account-link account="[[account]]"></gr-account-link>
<gr-button
id="remove"
+ link
hidden$="[[!removable]]"
hidden
tabindex="-1"
diff --git a/polygerrit-ui/app/elements/shared/gr-autocomplete-dropdown/gr-autocomplete-dropdown.html b/polygerrit-ui/app/elements/shared/gr-autocomplete-dropdown/gr-autocomplete-dropdown.html
index dcb38d4..c1c0338 100644
--- a/polygerrit-ui/app/elements/shared/gr-autocomplete-dropdown/gr-autocomplete-dropdown.html
+++ b/polygerrit-ui/app/elements/shared/gr-autocomplete-dropdown/gr-autocomplete-dropdown.html
@@ -15,7 +15,9 @@
-->
<link rel="import" href="../../../bower_components/polymer/polymer.html">
+
<link rel="import" href="../../../behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.html">
+<link rel="import" href="../../../bower_components/iron-dropdown/iron-dropdown.html">
<link rel="import" href="../../shared/gr-cursor-manager/gr-cursor-manager.html">
<script src="../../../scripts/rootElement.js"></script>
<link rel="import" href="../../../styles/shared-styles.html">
@@ -23,12 +25,6 @@
<dom-module id="gr-autocomplete-dropdown">
<template>
<style include="shared-styles">
- :host {
- background: #fff;
- box-shadow: rgba(0, 0, 0, 0.3) 0 1px 3px;
- position: absolute;
- z-index: 104;
- }
/* This must be set here vs. the container component because in some cases
the element is moved in the DOM to a base element and is no longer a
child of its original parent. */
@@ -48,19 +44,34 @@
li.selected {
background-color: #eee;
}
+ .dropdown-content {
+ background: #fff;
+ box-shadow: rgba(0, 0, 0, 0.3) 0 1px 3px;
+ }
</style>
- <div id="suggestions" role="listbox">
- <ul>
- <template is="dom-repeat" items="[[suggestions]]">
- <li data-index$="[[index]]"
- data-value$="[[item.dataValue]]"
- tabindex="-1"
- aria-label$="[[item.name]]"
- role="option"
- on-tap="_handleTapItem">[[item.text]]</li>
- </template>
- </ul>
- </div>
+ <iron-dropdown
+ id="dropdown"
+ allow-outside-scroll="true"
+ vertical-align="top"
+ horizontal-align="auto"
+ vertical-offset="[[verticalOffset]]">
+ <div
+ class="dropdown-content"
+ slot="dropdown-content"
+ id="suggestions"
+ role="listbox">
+ <ul>
+ <template is="dom-repeat" items="[[suggestions]]">
+ <li data-index$="[[index]]"
+ data-value$="[[item.dataValue]]"
+ tabindex="-1"
+ aria-label$="[[item.name]]"
+ role="option"
+ on-tap="_handleTapItem">[[item.text]]</li>
+ </template>
+ </ul>
+ </div>
+ </iron-dropdown>
<gr-cursor-manager
id="cursor"
index="{{index}}"
diff --git a/polygerrit-ui/app/elements/shared/gr-autocomplete-dropdown/gr-autocomplete-dropdown.js b/polygerrit-ui/app/elements/shared/gr-autocomplete-dropdown/gr-autocomplete-dropdown.js
index 12fb074..100b5ca 100644
--- a/polygerrit-ui/app/elements/shared/gr-autocomplete-dropdown/gr-autocomplete-dropdown.js
+++ b/polygerrit-ui/app/elements/shared/gr-autocomplete-dropdown/gr-autocomplete-dropdown.js
@@ -14,6 +14,9 @@
(function() {
'use strict';
+ const AWAIT_MAX_ITERS = 10;
+ const AWAIT_STEP = 5;
+
Polymer({
is: 'gr-autocomplete-dropdown',
@@ -31,8 +34,14 @@
properties: {
index: Number,
- moveToRoot: Boolean,
- fixedPosition: Boolean,
+ verticalOffset: {
+ type: Number,
+ value: null,
+ },
+ horizontalOffset: {
+ type: Number,
+ value: null,
+ },
suggestions: {
type: Array,
observer: '_resetCursorStops',
@@ -55,31 +64,47 @@
tab: '_handleTab',
},
- attached() {
- if (this.fixedPosition) {
- this.classList.add('fixed');
- }
- },
-
close() {
- if (this.moveToRoot) {
- Gerrit.getRootElement().removeChild(this);
- } else {
- this.hidden = true;
- }
+ this.$.dropdown.close();
},
open() {
- if (this.moveToRoot) {
- Gerrit.getRootElement().appendChild(this);
- }
- this._resetCursorStops();
- this._resetCursorIndex();
+ this._open().then(() => {
+ this._resetCursorStops();
+ this._resetCursorIndex();
+ this.fire('open-complete');
+ });
},
- setPosition(top, left) {
- this.style.top = top;
- this.style.left = left;
+ // TODO (beckysiegel) look into making this a behavior since it's used
+ // 3 times now.
+ _open(...args) {
+ return new Promise(resolve => {
+ Polymer.IronOverlayBehaviorImpl.open.apply(this.$.dropdown, args);
+ this._awaitOpen(resolve);
+ });
+ },
+
+ /**
+ * NOTE: (wyatta) Slightly hacky way to listen to the overlay actually
+ * opening. Eventually replace with a direct way to listen to the overlay.
+ */
+ _awaitOpen(fn) {
+ let iters = 0;
+ const step = () => {
+ this.async(() => {
+ if (this.style.display !== 'none') {
+ fn.call(this);
+ } else if (iters++ < AWAIT_MAX_ITERS) {
+ step.call(this);
+ }
+ }, AWAIT_STEP);
+ };
+ step.call(this);
+ },
+
+ get isHidden() {
+ return !this.$.dropdown.opened;
},
getCurrentText() {
@@ -134,9 +159,7 @@
_handleEscape() {
this._fireClose();
- if (!this.hidden) {
- this.close();
- }
+ this.close();
},
_handleTapItem(e) {
diff --git a/polygerrit-ui/app/elements/shared/gr-autocomplete-dropdown/gr-autocomplete-dropdown_test.html b/polygerrit-ui/app/elements/shared/gr-autocomplete-dropdown/gr-autocomplete-dropdown_test.html
index db9440c..23b27be 100644
--- a/polygerrit-ui/app/elements/shared/gr-autocomplete-dropdown/gr-autocomplete-dropdown_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-autocomplete-dropdown/gr-autocomplete-dropdown_test.html
@@ -27,13 +27,7 @@
<test-fixture id="basic">
<template>
- <gr-autocomplete-dropdown id="dropdown"></gr-autocomplete-dropdown>
- </template>
-</test-fixture>
-
-<test-fixture id="move">
- <template>
- <gr-autocomplete-dropdown id="dropdown" move-to-root></gr-autocomplete-dropdown>
+ <gr-autocomplete-dropdown></gr-autocomplete-dropdown>
</template>
</test-fixture>
@@ -49,6 +43,7 @@
element.suggestions = [
{dataValue: 'test value 1', name: 'test name 1', text: 1},
{dataValue: 'test value 2', name: 'test name 2', text: 2}];
+ flushAsynchronousOperations();
});
teardown(() => {
@@ -56,26 +51,12 @@
if (element.isOpen) element.close();
});
- test('dropdown has not been moved from text fixture to the body', () => {
- assert.equal(Polymer.dom(document.root)
- .querySelectorAll('gr-autocomplete-dropdown').length, 1);
- const dropdown = Polymer.dom(document.root)
- .querySelector('gr-autocomplete-dropdown');
- assert.isOk(dropdown);
- assert.notDeepEqual(dropdown.parentElement,
- Polymer.dom(document.root).querySelector('body'));
- });
-
- test('escape key', () => {
- const listener = sandbox.spy();
- element.hidden = false;
- element.addEventListener('dropdown-closed', listener);
- const closeSpy = sandbox.spy(element, 'close');
+ test('escape key', done => {
+ const closeSpy = sandbox.spy(element.$.dropdown, 'close');
MockInteractions.pressAndReleaseKeyOn(element, 27);
flushAsynchronousOperations();
- assert.isTrue(listener.called);
assert.isTrue(closeSpy.called);
- assert.isTrue(element.hidden);
+ done();
});
test('tab key', () => {
@@ -150,55 +131,4 @@
});
});
- suite('gr-autocomplete-dropdown to root', () => {
- let element;
- let sandbox;
-
- setup(() => {
- sandbox = sinon.sandbox.create();
- fixture('move').open();
- // The element was moved to the body, so look for it there.
- element = Polymer.dom(document.root)
- .querySelector('gr-autocomplete-dropdown');
- element.suggestions = [
- {dataValue: 'test value 1', name: 'test name 1', text: 1},
- {dataValue: 'test value 2', name: 'test name 2', text: 2}];
- });
-
- teardown(() => {
- sandbox.restore();
- if (!element.hidden) element.close();
- });
-
- test('dropdown has been moved from the text fixture to the body', () => {
- assert.equal(Polymer.dom(document.root)
- .querySelectorAll('gr-autocomplete-dropdown').length, 1);
- const dropdown = Polymer.dom(document.root)
- .querySelector('gr-autocomplete-dropdown');
- assert.isOk(dropdown);
- assert.deepEqual(dropdown.parentElement, Polymer.dom(document.root)
- .querySelector('body'));
- });
-
- test('closing removes from body and adding adds to body', () => {
- element.close();
- assert.equal(Polymer.dom(document.root)
- .querySelectorAll('gr-autocomplete-dropdown').length, 0);
- element.open();
- assert.equal(Polymer.dom(document.root)
- .querySelectorAll('gr-autocomplete-dropdown').length, 1);
- });
-
- test('setPosition', () => {
- const top = '10px';
- const left = '20px';
- element.setPosition(top, left);
- assert.equal(getComputedStyle(element).top, top);
- assert.equal(getComputedStyle(element).left, left);
- });
-
- test('getCurrentText', () => {
- assert.equal(element.getCurrentText(), 'test value 1');
- });
- });
</script>
diff --git a/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete.html b/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete.html
index 33d3cd9..81ac90e 100644
--- a/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete.html
+++ b/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete.html
@@ -57,8 +57,7 @@
on-keydown="_handleKeydown"
suggestions="[[_suggestions]]"
role="listbox"
- index="[[_index]]"
- hidden$="[[_computeSuggestionsHidden(_suggestions, _focused)]]">
+ index="[[_index]]">
</gr-autocomplete-dropdown>
</div>
</template>
diff --git a/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete.js b/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete.js
index 6df6c98..aa20f96 100644
--- a/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete.js
+++ b/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete.js
@@ -147,6 +147,7 @@
observers: [
'_textChanged(text)',
+ '_maybeOpenDropdown(_suggestions, _focused)',
],
_textChanged() {
@@ -230,8 +231,11 @@
});
},
- _computeSuggestionsHidden(suggestions, focused) {
- return !(suggestions.length && focused);
+ _maybeOpenDropdown(suggestions, focused) {
+ if (suggestions.length > 0 && focused) {
+ return this.$.suggestions.open();
+ }
+ return this.$.suggestions.close();
},
_computeClass(borderless) {
@@ -280,7 +284,7 @@
_cancel() {
if (this._suggestions.length) {
- this._suggestions = [];
+ this.set('_suggestions', []);
} else {
this.fire('cancel');
}
diff --git a/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete_test.html b/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete_test.html
index 038962c..fdfcddb 100644
--- a/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete_test.html
@@ -57,7 +57,7 @@
]);
});
element.query = queryStub;
- assert.isTrue(element.$.suggestions.hidden);
+ assert.isTrue(element.$.suggestions.isHidden);
assert.equal(element.$.suggestions.$.cursor.index, -1);
element.text = 'blah';
@@ -66,7 +66,7 @@
element._focused = true;
promise.then(() => {
- assert.isFalse(element.$.suggestions.hasAttribute('hidden'));
+ assert.isFalse(element.$.suggestions.isHidden);
const suggestions =
Polymer.dom(element.$.suggestions.root).querySelectorAll('li');
assert.equal(suggestions.length, 5);
@@ -89,20 +89,20 @@
});
element.query = queryStub;
- assert.isTrue(element.$.suggestions.hidden);
+ assert.isTrue(element.$.suggestions.isHidden);
element._focused = true;
element.text = 'blah';
promise.then(() => {
- assert.isFalse(element.$.suggestions.hidden);
+ assert.isFalse(element.$.suggestions.isHidden);
const cancelHandler = sandbox.spy();
element.addEventListener('cancel', cancelHandler);
MockInteractions.pressAndReleaseKeyOn(element.$.input, 27, null, 'esc');
assert.isFalse(cancelHandler.called);
- assert.isTrue(element.$.suggestions.hidden);
+ assert.isTrue(element.$.suggestions.isHidden);
assert.equal(element._suggestions.length, 0);
MockInteractions.pressAndReleaseKeyOn(element.$.input, 27, null, 'esc');
@@ -124,13 +124,13 @@
});
element.query = queryStub;
- assert.isTrue(element.$.suggestions.hidden);
+ assert.isTrue(element.$.suggestions.isHidden);
assert.equal(element.$.suggestions.$.cursor.index, -1);
element._focused = true;
element.text = 'blah';
promise.then(() => {
- assert.isFalse(element.$.suggestions.hidden);
+ assert.isFalse(element.$.suggestions.isHidden);
const commitHandler = sandbox.spy();
element.addEventListener('commit', commitHandler);
@@ -157,7 +157,7 @@
assert.equal(element.value, 1);
assert.isTrue(commitHandler.called);
assert.equal(commitHandler.getCall(0).args[0].detail.value, 1);
- assert.isTrue(element.$.suggestions.hidden);
+ assert.isTrue(element.$.suggestions.isHidden);
assert.isTrue(element._focused);
done();
});
@@ -291,9 +291,16 @@
});
test('_focused flag shows/hides the suggestions', () => {
- const suggestions = ['hello', 'its me'];
- assert.isTrue(element._computeSuggestionsHidden(suggestions, false));
- assert.isFalse(element._computeSuggestionsHidden(suggestions, true));
+ const openStub = sandbox.stub(element.$.suggestions, 'open');
+ const closedStub = sandbox.stub(element.$.suggestions, 'close');
+ element._suggestions = ['hello', 'its me'];
+ assert.isFalse(openStub.called);
+ assert.isTrue(closedStub.calledOnce);
+ element._focused = true;
+ assert.isTrue(openStub.calledOnce);
+ element._suggestions = [];
+ assert.isTrue(closedStub.calledTwice);
+ assert.isTrue(openStub.calledOnce);
});
test('changing input sets _textChangedSinceCommit', () => {
@@ -375,7 +382,7 @@
element.tabComplete = false;
focusSpy = sandbox.spy(element, 'focus');
Polymer.dom.flush();
- assert.isFalse(element.$.suggestions.hidden);
+ assert.isFalse(element.$.suggestions.isHidden);
MockInteractions.pressAndReleaseKeyOn(
element.$.suggestions.$$('li:first-child'), 9, null, 'tab');
@@ -391,7 +398,7 @@
element.tabComplete = true;
focusSpy = sandbox.spy(element, 'focus');
Polymer.dom.flush();
- assert.isFalse(element.$.suggestions.hidden);
+ assert.isFalse(element.$.suggestions.isHidden);
MockInteractions.pressAndReleaseKeyOn(
element.$.suggestions.$$('li:first-child'), 9, null, 'tab');
@@ -406,13 +413,13 @@
element._focused = true;
element._suggestions = [{name: 'first suggestion'}];
Polymer.dom.flush();
- assert.isFalse(element.$.suggestions.hidden);
+ assert.isFalse(element.$.suggestions.isHidden);
MockInteractions.tap(element.$.suggestions.$$('li:first-child'));
flushAsynchronousOperations();
assert.isFalse(focusSpy.called);
assert.isTrue(commitSpy.called);
- assert.isTrue(element.$.suggestions.hidden);
+ assert.isTrue(element.$.suggestions.isHidden);
});
});
diff --git a/polygerrit-ui/app/elements/shared/gr-button/gr-button.html b/polygerrit-ui/app/elements/shared/gr-button/gr-button.html
index c194bcb..f63d7923 100644
--- a/polygerrit-ui/app/elements/shared/gr-button/gr-button.html
+++ b/polygerrit-ui/app/elements/shared/gr-button/gr-button.html
@@ -18,122 +18,100 @@
<link rel="import" href="../../../behaviors/gr-tooltip-behavior/gr-tooltip-behavior.html">
<link rel="import" href="../../../behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.html">
+<link rel="import" href="../../../bower_components/paper-button/paper-button.html">
<link rel="import" href="../../../styles/shared-styles.html">
<dom-module id="gr-button">
<template strip-whitespace>
<style include="shared-styles">
:host {
- background-color: #f5f5f5;
- border: 1px solid #d1d2d3;
- border-radius: 2px;
- box-sizing: border-box;
- color: #333;
- cursor: pointer;
display: inline-block;
font-family: var(--font-family-bold);
font-size: 12px;
- outline-width: 0;
- padding: .4em .85em;
position: relative;
- text-align: center;
- -moz-user-select: none;
- -ms-user-select: none;
- -webkit-user-select: none;
- user-select: none;
}
:host([hidden]) {
display: none;
}
- :host([primary]),
- :host([secondary]) {
- color: #fff;
- }
- :host([primary]) {
- background-color: #4d90fe;
- border-color: #3079ed;
- }
- :host([secondary]) {
- background-color: #d14836;
- border-color: transparent;
- }
- :host([small]) {
- font-size: 12px;
- }
:host([link]) {
background-color: transparent;
border: none;
- color: #00f;
+ color: var(--color-link);
font-size: inherit;
- font-family: var(--font-family);
- padding: 0;
- text-decoration: underline;
+ font-family: var(--font-family-bold);
+ text-transform: none;
}
- :host([loading]),
- :host([disabled]) {
+ :host([link]) paper-button {
+ margin: 0;
+ padding: 0;
+ @apply --gr-button;
+ }
+ paper-button[raised] {
+ background-color: var(--gr-button-background, #fff);
+ color: var(--gr-button-color, --color-link);
+ }
+ /* todo (beckysiegel) switch all secondary to primary as there is no color
+ distinction anymore. */
+ :host([primary]) paper-button[raised],
+ :host([secondary]) paper-button[raised] {
+ background-color: var(--color-link);
+ color: #fff;
+ }
+ :host([link]) paper-button:hover,
+ :host([link]) paper-button:focus,
+ paper-button[raised]:hover,
+ paper-button[raised]:focus {
+ color: var(--gr-button-hover-color, --color-button-hover);
+ }
+ :host([primary]) paper-button[raised]:hover,
+ :host([primary]) paper-button[raised]:focus,
+ :host([secondary]) paper-button[raised]:hover,
+ :host([secondary]) paper-button[raised]:focus {
+ background-color: var(--gr-button-hover-background-color, --color-button-hover);
+ color: var(--gr-button-color, #fff);
+ }
+ paper-button,
+ paper-button[raised],
+ paper-button[link] {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ margin: 0;
+ min-width: 0;
+ padding: .4em .85em;
+ @apply --gr-button;
+ }
+ :host([link]) paper-button {
+ --paper-button: {
+ padding: 0;
+ }
+ }
+ :host:not([down-arrow]) .downArrow {display: none; }
+ :host([down-arrow]) .downArrow {
+ border-left: .36em solid transparent;
+ border-right: .36em solid transparent;
+ border-top: .36em solid #ccc;
+ margin-left: .5em;
+ transition: border-top-color 200ms;
+ }
+ :host([down-arrow]):hover .downArrow {
+ border-top-color: #666;
+ }
+ :host([loading]) paper-button,
+ :host([disabled]) paper-button {
+ color: #aaa;
+ }
+ :host([loading]) paper-button,
+ :host([loading][disabled]) paper-button {
+ cursor: wait;
background-color: #efefef;
color: #aaa;
}
- :host([disabled]) {
- cursor: default;
- }
- :host([loading]),
- :host([loading][disabled]) {
- cursor: wait;
- }
- :host:focus:not([link]),
- :host:hover:not([link]) {
- background-color: #f8f8f8;
- border-color: #aaa;
- }
- :host(:active) {
- border-color: #d1d2d3;
- color: #aaa;
- }
- :host([primary]:focus),
- :host([secondary]:focus),
- :host([primary]:active),
- :host([secondary]:active) {
- color: #fff;
- }
- :host([primary]:focus) {
- box-shadow: 0 0 1px #00f;
- background-color: #4d90fe;
- }
- :host([primary]:not([disabled]):hover) {
- background-color: #4d90fe;
- border-color: #00F;
- }
- :host([primary]:active),
- :host([secondary]:active) {
- box-shadow: none;
- }
- :host([primary]:active) {
- border-color: #0c2188;
- }
- :host([secondary]:focus) {
- box-shadow: 0 0 1px #f00;
- background-color: #d14836;
- }
- :host([secondary]:not([disabled]):hover) {
- background-color: #c53727;
- border: 1px solid #b0281a;
- }
- :host([secondary]:active) {
- border-color: #941c0c;
- }
- :host([primary][loading]) {
- background-color: #7caeff;
- border-color: transparent;
- color: #fff;
- }
- :host([primary][disabled]) {
- background-color: #4d90fe;
- color: #fff;
- opacity: .5;
- }
</style>
- <content></content>
+ <paper-button raised="[[!link]]">
+ <content></content>
+ <i class="downArrow"></i>
+ </paper-button>
</template>
<script src="gr-button.js"></script>
-</dom-module>
+</dom-module>
\ No newline at end of file
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 ddb2bc3..80c0a9b 100644
--- a/polygerrit-ui/app/elements/shared/gr-button/gr-button.js
+++ b/polygerrit-ui/app/elements/shared/gr-button/gr-button.js
@@ -18,8 +18,13 @@
is: 'gr-button',
properties: {
+ downArrow: {
+ type: Boolean,
+ reflectToAttribute: true,
+ },
link: {
type: Boolean,
+ value: false,
reflectToAttribute: true,
},
disabled: {
diff --git a/polygerrit-ui/app/elements/shared/gr-dropdown-list/gr-dropdown-list.html b/polygerrit-ui/app/elements/shared/gr-dropdown-list/gr-dropdown-list.html
index 59f63fa..8a3de33 100644
--- a/polygerrit-ui/app/elements/shared/gr-dropdown-list/gr-dropdown-list.html
+++ b/polygerrit-ui/app/elements/shared/gr-dropdown-list/gr-dropdown-list.html
@@ -62,6 +62,7 @@
paper-item {
cursor: pointer;
flex-direction: column;
+ font-size: 1em;
--paper-item: {
min-height: 0;
padding: 10px 16px;
@@ -79,7 +80,7 @@
paper-item:not(:last-of-type) {
border-bottom: 1px solid #ddd;
}
- gr-button {
+ #trigger {
color: black;
font: inherit;
padding: .3em 0;
@@ -87,7 +88,7 @@
}
.bottomContent {
color: rgba(0,0,0,.54);
- font-size: .85em;
+ font-size: .9em;
line-height: 16px;
}
.bottomContent,
@@ -105,6 +106,16 @@
gr-select {
display: none;
}
+ /* Because the iron dropdown 'area' includes the trigger, and the entire
+ width of the dropdown, we want to treat tapping the area above the
+ dropdown content as if it is tapping whatever content is underneath it.
+ The next two styles allow this to happen. */
+ iron-dropdown {
+ pointer-events: none;
+ }
+ paper-listbox {
+ pointer-events: auto;
+ }
@media only screen and (max-width: 50em) {
gr-select {
display: inline;
@@ -122,7 +133,8 @@
link
id="trigger"
class="dropdown-trigger"
- on-tap="_showDropdownTapHandler">
+ on-tap="_showDropdownTapHandler"
+ slot="dropdown-trigger">
<span>[[text]]</span>
<span
class="downArrow"
@@ -131,13 +143,14 @@
<iron-dropdown
id="dropdown"
vertical-align="top"
- allow-outside-scroll="true">
+ allow-outside-scroll="true"
+ on-tap="_handleDropdownTap">
<paper-listbox
class="dropdown-content"
slot="dropdown-content"
attr-for-selected="value"
- on-tap="_handleDropdownTap"
- selected="{{value}}">
+ selected="{{value}}"
+ on-tap="_handleDropdownTap">
<template is="dom-repeat" items="[[items]]">
<paper-item
disabled="[[item.disabled]]"
diff --git a/polygerrit-ui/app/elements/shared/gr-dropdown-list/gr-dropdown-list.js b/polygerrit-ui/app/elements/shared/gr-dropdown-list/gr-dropdown-list.js
index 27c6ba8..dc8b44e 100644
--- a/polygerrit-ui/app/elements/shared/gr-dropdown-list/gr-dropdown-list.js
+++ b/polygerrit-ui/app/elements/shared/gr-dropdown-list/gr-dropdown-list.js
@@ -95,6 +95,7 @@
const selectedObj = items.find(item => {
return item.value + '' === value + '';
});
+ if (!selectedObj) { return; }
this.text = selectedObj.triggerText? selectedObj.triggerText :
selectedObj.text;
},
diff --git a/polygerrit-ui/app/elements/shared/gr-dropdown/gr-dropdown.html b/polygerrit-ui/app/elements/shared/gr-dropdown/gr-dropdown.html
index f4813fa..b821909 100644
--- a/polygerrit-ui/app/elements/shared/gr-dropdown/gr-dropdown.html
+++ b/polygerrit-ui/app/elements/shared/gr-dropdown/gr-dropdown.html
@@ -43,9 +43,6 @@
font: inherit;
padding: .3em 0;
}
- :host([down-arrow]) .dropdown-trigger {
- padding-right: 1.4em;
- }
gr-avatar {
height: 2em;
width: 2em;
@@ -75,7 +72,6 @@
}
li .itemAction:link,
li .itemAction:visited {
- color: #00e;
text-decoration: none;
}
li .itemAction:not(.disabled):hover {
@@ -94,26 +90,13 @@
.bold-text {
font-family: var(--font-family-bold);
}
- :host:not([down-arrow]) .downArrow { display: none; }
- :host([down-arrow]) .downArrow {
- border-left: .36em solid transparent;
- border-right: .36em solid transparent;
- border-top: .36em solid #ccc;
- height: 0;
- position: absolute;
- right: .3em;
- top: calc(50% - .05em);
- transition: border-top-color 200ms;
- width: 0;
- }
- .dropdown-trigger:hover .downArrow {
- border-top-color: #666;
- }
</style>
- <gr-button link="[[link]]" class="dropdown-trigger" id="trigger"
+ <gr-button
+ link="[[link]]"
+ class="dropdown-trigger" id="trigger"
+ down-arrow="[[downArrow]]"
on-tap="_showDropdownTapHandler">
<content></content>
- <i class="downArrow"></i>
</gr-button>
<iron-dropdown id="dropdown"
vertical-align="top"
diff --git a/polygerrit-ui/app/elements/shared/gr-dropdown/gr-dropdown.js b/polygerrit-ui/app/elements/shared/gr-dropdown/gr-dropdown.js
index ec8f04c..49e5a5e 100644
--- a/polygerrit-ui/app/elements/shared/gr-dropdown/gr-dropdown.js
+++ b/polygerrit-ui/app/elements/shared/gr-dropdown/gr-dropdown.js
@@ -37,6 +37,7 @@
type: Array,
observer: '_resetCursorStops',
},
+ downArrow: Boolean,
topContent: Object,
horizontalAlign: {
type: String,
diff --git a/polygerrit-ui/app/elements/shared/gr-editable-label/gr-editable-label.html b/polygerrit-ui/app/elements/shared/gr-editable-label/gr-editable-label.html
index 79d69f5..6eb7c8d 100644
--- a/polygerrit-ui/app/elements/shared/gr-editable-label/gr-editable-label.html
+++ b/polygerrit-ui/app/elements/shared/gr-editable-label/gr-editable-label.html
@@ -27,6 +27,9 @@
align-items: center;
display: inline-flex;
}
+ :host([uppercase]) label {
+ text-transform: uppercase;
+ }
input,
label {
width: 100%;
@@ -37,14 +40,14 @@
label {
color: #777;
display: inline-block;
+ font-family: var(--font-family-bold);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
label.editable {
- color: #00f;
+ color: var(--color-link);
cursor: pointer;
- text-decoration: underline;
}
#dropdown {
box-shadow: rgba(0, 0, 0, 0.3) 0 1px 3px;
diff --git a/polygerrit-ui/app/elements/shared/gr-editable-label/gr-editable-label.js b/polygerrit-ui/app/elements/shared/gr-editable-label/gr-editable-label.js
index f87a546..b0e8516 100644
--- a/polygerrit-ui/app/elements/shared/gr-editable-label/gr-editable-label.js
+++ b/polygerrit-ui/app/elements/shared/gr-editable-label/gr-editable-label.js
@@ -46,6 +46,11 @@
type: Boolean,
value: false,
},
+ uppercase: {
+ type: Boolean,
+ reflectToAttribute: true,
+ value: false,
+ },
_inputText: String,
// This is used to push the iron-input element up on the page, so
// the input is placed in approximately the same position as the
diff --git a/polygerrit-ui/app/elements/shared/gr-linked-chip/gr-linked-chip.html b/polygerrit-ui/app/elements/shared/gr-linked-chip/gr-linked-chip.html
index be71eb6..d30bad2 100644
--- a/polygerrit-ui/app/elements/shared/gr-linked-chip/gr-linked-chip.html
+++ b/polygerrit-ui/app/elements/shared/gr-linked-chip/gr-linked-chip.html
@@ -34,24 +34,31 @@
display: inline-flex;
padding: 0 .5em;
}
- gr-button.remove,
gr-button.remove:hover,
gr-button.remove:focus {
- border-color: transparent;
- color: #333;
+ --gr-button: {
+ color: #333;
+ }
}
gr-button.remove {
- background: #eee;
- border: 0;
- color: #666;
- font-size: 1.7em;
- font-weight: normal;
- height: .6em;
- line-height: .6em;
- margin-left: .15em;
- margin-top: -.05em;
- padding: 0;
- text-decoration: none;
+ --gr-button: {
+ border: 0;
+ color: #666;
+ font-size: 1.7em;
+ font-weight: normal;
+ height: .6em;
+ line-height: .6em;
+ margin-left: .15em;
+ margin-top: -.05em;
+ padding: 0;
+ text-decoration: none;
+ }
+ --gr-button-hover-color: {
+ color: #333;
+ }
+ --gr-button-hover-background-color: {
+ color: #333;
+ }
}
.transparentBackground,
gr-button.transparentBackground {
@@ -68,6 +75,7 @@
</a>
<gr-button
id="remove"
+ link
hidden$="[[!removable]]"
hidden
class$="remove [[_getBackgroundClass(transparentBackground)]]"
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 4e0f3b7..5153fb0 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
@@ -486,6 +486,14 @@
});
},
+ /**
+ * @param {string} userId the ID of the user usch as an email address.
+ * @return {!Promise<!Object>}
+ */
+ getAccountDetails(userId) {
+ return this.fetchJSON(`/accounts/${encodeURIComponent(userId)}/detail`);
+ },
+
getAccountEmails() {
return this._fetchSharedCacheURL('/accounts/self/emails');
},
@@ -579,6 +587,10 @@
});
},
+ getAccountStatus(userId) {
+ return this.fetchJSON(`/accounts/${encodeURIComponent(userId)}/status`);
+ },
+
getAccountGroups() {
return this._fetchSharedCacheURL('/accounts/self/groups');
},
diff --git a/polygerrit-ui/app/elements/shared/gr-textarea/gr-textarea.html b/polygerrit-ui/app/elements/shared/gr-textarea/gr-textarea.html
index 59daf07..95df7c9 100644
--- a/polygerrit-ui/app/elements/shared/gr-textarea/gr-textarea.html
+++ b/polygerrit-ui/app/elements/shared/gr-textarea/gr-textarea.html
@@ -28,10 +28,14 @@
<style include="shared-styles">
:host {
display: block;
+ position: relative;
}
:host(.monospace) {
font-family: var(--monospace-font-family);
}
+ #emojiSuggestions {
+ font-family: var(--font-family);
+ }
gr-autocomplete {
display: inline-block
}
@@ -39,11 +43,16 @@
background-color: var(--background-color, none);
width: 100%;
}
+ #hiddenText #emojiSuggestions {
+ visibility: visible;
+ white-space: normal;
+ }
/*This is needed to not add a scroll bar on the side of gr-textarea
since there is 2px of padding in iron-autogrow-textarea for the
native textarea*/
iron-autogrow-textarea {
padding: 2px;
+ position: relative;
}
#textarea.noBorder {
border: none;
@@ -53,17 +62,19 @@
float: left;
position: absolute;
visibility: hidden;
- white-space: pre-wrap
+ width: 100%;
+ white-space: pre-wrap;
}
</style>
- <gr-autocomplete-dropdown id="emojiSuggestions"
+ <div id="hiddenText"></div>
+ <gr-autocomplete-dropdown
+ id="emojiSuggestions"
suggestions="[[_suggestions]]"
index="[[_index]]"
- move-to-root
- fixed-position="[[fixedPositionDropdown]]"
- hidden>
+ vertical-offset="[[_verticalOffset]]"
+ on-dropdown-closed="_resetAndFocus"
+ on-item-selected="_handleEmojiSelect">
</gr-autocomplete-dropdown>
- <div id="hiddenText"></div>
<iron-autogrow-textarea
id="textarea"
autocomplete="[[autocomplete]]"
diff --git a/polygerrit-ui/app/elements/shared/gr-textarea/gr-textarea.js b/polygerrit-ui/app/elements/shared/gr-textarea/gr-textarea.js
index a172a09..2d796bd 100644
--- a/polygerrit-ui/app/elements/shared/gr-textarea/gr-textarea.js
+++ b/polygerrit-ui/app/elements/shared/gr-textarea/gr-textarea.js
@@ -15,7 +15,6 @@
'use strict';
const MAX_ITEMS_DROPDOWN = 10;
- const VERTICAL_OFFSET = 7;
const ALL_SUGGESTIONS = [
{value: '💯', match: '100'},
@@ -63,8 +62,6 @@
rows: Number,
maxRows: Number,
placeholder: String,
- fixedPositionDropdown: Boolean,
- moveToRoot: Boolean,
text: {
type: String,
notify: true,
@@ -95,6 +92,12 @@
},
_index: Number,
_suggestions: Array,
+ // Offset makes dropdown appear below text.
+ _verticalOffset: {
+ type: Number,
+ value: 20,
+ readOnly: true,
+ },
},
behaviors: [
@@ -120,22 +123,11 @@
if (this.backgroundColor) {
this.updateStyles({'--background-color': this.backgroundColor});
}
- this.listen(this.$.emojiSuggestions, 'dropdown-closed', '_resetAndFocus');
- this.listen(this.$.emojiSuggestions, 'item-selected',
- '_handleEmojiSelect');
},
- detached() {
- this.closeDropdown();
- this.listen(this.$.emojiSuggestions, 'dropdown-closed', '_resetAndFocus');
- this.listen(this.$.emojiSuggestions, 'item-selected',
- '_handleEmojiSelect');
- },
closeDropdown() {
- if (!this.$.emojiSuggestions.hidden) {
- this._closeEmojiDropdown();
- }
+ return this.$.emojiSuggestions.close();
},
getNativeTextarea() {
@@ -161,7 +153,6 @@
_resetAndFocus() {
this._resetEmojiDropdown();
- this.$.textarea.textarea.focus();
},
_handleUpKey(e) {
@@ -197,14 +188,17 @@
return this.text.substr(0, this._colonIndex || 0) +
value + this.text.substr(this.$.textarea.selectionStart) + ' ';
},
-
- _getPositionOfCursor() {
+ /**
+ * Uses a hidden element with the same width and styling of the textarea and
+ * the text up until the point of interest. Then the emoji selection
+ * element is added to the end so that they are correctly positioned by the
+ * end of the last character entered.
+ */
+ _updateCaratPosition() {
this.$.hiddenText.textContent = this.$.textarea.value.substr(0,
this.$.textarea.selectionStart);
- const caratSpan = document.createElement('span');
- this.$.hiddenText.appendChild(caratSpan);
- return caratSpan.getBoundingClientRect();
+ this.$.hiddenText.appendChild(this.$.emojiSuggestions);
},
_getFontSize() {
@@ -218,29 +212,6 @@
},
/**
- * This positions the dropdown to be just below the cursor position. It is
- * calculated by having a hidden element with the same width and styling of
- * the tetarea and the text up until the point of interest. Then a span
- * element is added to the end so that there is a specific element to get
- * the position of. Line height is determined (or falls back to 12px) as
- * extra height to add.
- */
- _updateSelectorPosition() {
- // These are broken out into separate functions for testability.
- const caratPosition = this._getPositionOfCursor();
- const fontSize = this._getFontSize();
-
- let top = caratPosition.top + fontSize + VERTICAL_OFFSET;
-
- if (!this.fixedPositionDropdown) {
- top += this._getScrollTop();
- }
- top += 'px';
- const left = caratPosition.left + 'px';
- this.$.emojiSuggestions.setPosition(top, left);
- },
-
- /**
* _handleKeydown used for key handling in the this.$.textarea AND all child
* autocomplete options.
*/
@@ -278,23 +249,16 @@
this._resetEmojiDropdown();
// Otherwise open the dropdown and set the position to be just below the
// cursor.
- } else if (this.$.emojiSuggestions.hidden) {
+ } else if (this.$.emojiSuggestions.isHidden) {
this._hideAutocomplete = false;
this._openEmojiDropdown();
- this._updateSelectorPosition();
+ this._updateCaratPosition();
}
this.$.textarea.textarea.focus();
}
},
-
- _closeEmojiDropdown() {
- this.$.emojiSuggestions.close();
- this.$.emojiSuggestions.hidden = true;
- },
-
_openEmojiDropdown() {
this.$.emojiSuggestions.open();
- this.$.emojiSuggestions.hidden = false;
},
_formatSuggestions(matchedSuggestions) {
diff --git a/polygerrit-ui/app/elements/shared/gr-textarea/gr-textarea_test.html b/polygerrit-ui/app/elements/shared/gr-textarea/gr-textarea_test.html
index 95e3a8d..493dd5d 100644
--- a/polygerrit-ui/app/elements/shared/gr-textarea/gr-textarea_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-textarea/gr-textarea_test.html
@@ -72,7 +72,7 @@
element.$.textarea.selectionStart = 1;
element.$.textarea.selectionEnd = 1;
element.text = ':';
- assert.isFalse(!element.$.emojiSuggestions.hidden);
+ assert.isFalse(!element.$.emojiSuggestions.isHidden);
});
test('emoji selector is not open when a general text is entered', () => {
@@ -80,7 +80,7 @@
element.$.textarea.selectionStart = 9;
element.$.textarea.selectionEnd = 9;
element.text = 'some text';
- assert.isFalse(!element.$.emojiSuggestions.hidden);
+ assert.isFalse(!element.$.emojiSuggestions.isHidden);
});
test('emoji selector opens when a colon is typed & the textarea has focus',
@@ -95,7 +95,7 @@
element.$.textarea.selectionEnd = 2;
element.text = ':t';
flushAsynchronousOperations();
- assert.isFalse(element.$.emojiSuggestions.hidden);
+ assert.isFalse(element.$.emojiSuggestions.isHidden);
assert.equal(element._colonIndex, 0);
assert.isFalse(element._hideAutocomplete);
assert.equal(element._currentSearchString, 't');
@@ -165,32 +165,16 @@
assert.equal(element.text, 'test test 😂 ');
});
- test('_getPositionOfCursor', () => {
+ test('_updateCaratPosition', () => {
element.$.textarea.selectionStart = 4;
element.$.textarea.selectionEnd = 4;
element.text = 'test';
- element._getPositionOfCursor();
+ element._updateCaratPosition();
assert.deepEqual(element.$.hiddenText.innerHTML, element.text +
- '<span></span>');
+ element.$.emojiSuggestions.outerHTML);
});
- test('_updateSelectorPosition', () => {
- const setPositionSpy =
- sandbox.spy(element.$.emojiSuggestions, 'setPosition');
- sandbox.stub(element, '_getPositionOfCursor', () => {
- return {top: 100, left: 30};
- });
- sandbox.stub(element, '_getFontSize', () => 12);
- sandbox.stub(element, '_getScrollTop', () => 100);
- element._updateSelectorPosition();
- assert.isTrue(setPositionSpy.lastCall.calledWithExactly('219px', '30px'));
-
- element.fixedPositionDropdown = true;
- element._updateSelectorPosition();
- assert.isTrue(setPositionSpy.lastCall.calledWithExactly('119px', '30px'));
- });
-
- test('emoji dropdown is closed when dropdown-closed is fired', () => {
+ test('emoji dropdown is closed when iron-overlay-closed is fired', () => {
const resetSpy = sandbox.spy(element, '_resetAndFocus');
element.$.emojiSuggestions.fire('dropdown-closed');
assert.isTrue(resetSpy.called);
@@ -205,56 +189,67 @@
});
suite('keyboard shortcuts', () => {
- function setupDropdown() {
- MockInteractions.focus(element.$.textarea);
+ function setupDropdown(callback) {
+ element.$.emojiSuggestions.addEventListener('open-complete', () => {
+ callback();
+ });
flushAsynchronousOperations();
+ MockInteractions.focus(element.$.textarea);
element.$.textarea.selectionStart = 1;
element.$.textarea.selectionEnd = 1;
element.text = ':';
element.$.textarea.selectionStart = 1;
- element.$.textarea.selectionEnd = 1;
+ element.$.textarea.selectionEnd = 2;
element.text = ':1';
}
- test('escape key', () => {
+ test('escape key', done => {
const resestSpy = sandbox.spy(element, '_resetAndFocus');
MockInteractions.pressAndReleaseKeyOn(element.$.textarea, 27);
assert.isFalse(resestSpy.called);
- setupDropdown();
- MockInteractions.pressAndReleaseKeyOn(element.$.textarea, 27);
- assert.isTrue(resestSpy.called);
- assert.isFalse(!element.$.emojiSuggestions.hidden);
+ setupDropdown(() => {
+ MockInteractions.pressAndReleaseKeyOn(element.$.textarea, 27);
+ assert.isTrue(resestSpy.called);
+ assert.isFalse(!element.$.emojiSuggestions.isHidden);
+ done();
+ });
});
- test('up key', () => {
+ test('up key', done => {
const upSpy = sandbox.spy(element.$.emojiSuggestions, 'cursorUp');
MockInteractions.pressAndReleaseKeyOn(element.$.textarea, 38);
assert.isFalse(upSpy.called);
- setupDropdown();
- MockInteractions.pressAndReleaseKeyOn(element.$.textarea, 38);
- assert.isTrue(upSpy.called);
+ setupDropdown(() => {
+ MockInteractions.pressAndReleaseKeyOn(element.$.textarea, 38);
+ assert.isTrue(upSpy.called);
+ done();
+ });
});
- test('down key', () => {
+ test('down key', done => {
const downSpy = sandbox.spy(element.$.emojiSuggestions, 'cursorDown');
MockInteractions.pressAndReleaseKeyOn(element.$.textarea, 40);
assert.isFalse(downSpy.called);
- setupDropdown();
- MockInteractions.pressAndReleaseKeyOn(element.$.textarea, 40);
- assert.isTrue(downSpy.called);
+ setupDropdown(() => {
+ MockInteractions.pressAndReleaseKeyOn(element.$.textarea, 40);
+ assert.isTrue(downSpy.called);
+ done();
+ });
});
- test('enter key', () => {
+ test('enter key', done => {
const enterSpy = sandbox.spy(element.$.emojiSuggestions,
'getCursorTarget');
MockInteractions.pressAndReleaseKeyOn(element.$.textarea, 13);
assert.isFalse(enterSpy.called);
- setupDropdown();
- MockInteractions.pressAndReleaseKeyOn(element.$.textarea, 13);
- assert.isTrue(enterSpy.called);
- flushAsynchronousOperations();
- // A space is automatically added at the end.
- assert.equal(element.text, '💯 ');
+ setupDropdown(() => {
+ MockInteractions.pressAndReleaseKeyOn(element.$.textarea, 13);
+ assert.isTrue(enterSpy.called);
+ flushAsynchronousOperations();
+ // A space is automatically added at the end.
+ assert.equal(element.text, '💯 ');
+ done();
+ });
});
});
});
diff --git a/polygerrit-ui/app/scripts/util.js b/polygerrit-ui/app/scripts/util.js
index d68433c..573335c 100644
--- a/polygerrit-ui/app/scripts/util.js
+++ b/polygerrit-ui/app/scripts/util.js
@@ -38,25 +38,5 @@
}
return '';
};
-
- /**
- * Truncates URLs to display filename only
- * Example
- * // returns '.../text.html'
- * util.truncatePath.('dir/text.html');
- * Example
- * // returns 'text.html'
- * util.truncatePath.('text.html');
- * @return {string} Returns the truncated value of a URL.
- */
- util.truncatePath = function(path) {
- const pathPieces = path.split('/');
-
- if (pathPieces.length < 2) {
- return path;
- }
- // Character is an ellipsis.
- return '\u2026/' + pathPieces[pathPieces.length - 1];
- };
window.util = util;
})(window);
diff --git a/polygerrit-ui/app/styles/app-theme.html b/polygerrit-ui/app/styles/app-theme.html
index 4318757..6a81158 100644
--- a/polygerrit-ui/app/styles/app-theme.html
+++ b/polygerrit-ui/app/styles/app-theme.html
@@ -35,6 +35,11 @@
--iron-overlay-backdrop: {
transition: none;
}
+
+ /* Follow are a part of the design refresh */
+ --color-link: #2a66d9;
+ /* 12% darker */
+ --color-button-hover: #0B47BA;
}
@media screen and (max-width: 50em) {
:root {
diff --git a/polygerrit-ui/app/styles/shared-styles.html b/polygerrit-ui/app/styles/shared-styles.html
index 5c11d60..7389fa4 100644
--- a/polygerrit-ui/app/styles/shared-styles.html
+++ b/polygerrit-ui/app/styles/shared-styles.html
@@ -38,6 +38,9 @@
margin: 0;
padding: 0;
}
+ a {
+ color: var(--color-link);
+ }
input,
textarea,
select,
diff --git a/polygerrit-ui/app/test/index.html b/polygerrit-ui/app/test/index.html
index c748a9b..7080eb7 100644
--- a/polygerrit-ui/app/test/index.html
+++ b/polygerrit-ui/app/test/index.html
@@ -52,6 +52,7 @@
'change-list/gr-change-list-item/gr-change-list-item_test.html',
'change-list/gr-change-list-view/gr-change-list-view_test.html',
'change-list/gr-change-list/gr-change-list_test.html',
+ 'change-list/gr-user-header/gr-user-header_test.html',
'change/gr-account-entry/gr-account-entry_test.html',
'change/gr-account-list/gr-account-list_test.html',
'change/gr-change-actions/gr-change-actions_test.html',
@@ -160,6 +161,7 @@
// Behaviors tests.
const behaviors = [
+ 'async-foreach-behavior/async-foreach-behavior_test.html',
'base-url-behavior/base-url-behavior_test.html',
'docs-url-behavior/docs-url-behavior_test.html',
'keyboard-shortcut-behavior/keyboard-shortcut-behavior_test.html',