Merge changes Idf06a875,I6b10e93f
* changes:
Make ChangeControl#getRange private and migrate caller
Migrate ApprovalsUtil#checkApprovals() to PermissionBackend
diff --git a/Documentation/dev-plugins-pg.txt b/Documentation/dev-plugins-pg.txt
index 9d36758..e1bf39e 100644
--- a/Documentation/dev-plugins-pg.txt
+++ b/Documentation/dev-plugins-pg.txt
@@ -45,14 +45,14 @@
decoration case, a hook is set with a `content` attribute that points to the DOM
element.
-1. Get the DOM hook API instance via `plugin.getDomHook(endpointName)`
+1. Get the DOM hook API instance via `plugin.hook(endpointName)`
2. Set up an `onAttached` callback
3. Callback is called when the hook element is created and inserted into DOM
4. Use element.content to get UI element
``` js
Gerrit.install(function(plugin) {
- const domHook = plugin.getDomHook('reply-text');
+ const domHook = plugin.hook('reply-text');
domHook.onAttached(element => {
if (!element.content) { return; }
// element.content is a reply dialog text area.
@@ -70,7 +70,7 @@
``` js
Gerrit.install(function(plugin) {
- const domHook = plugin.getDomHook('reply-text');
+ const domHook = plugin.hook('reply-text');
domHook.onAttached(element => {
if (!element.content) { return; }
element.content.style.border = '1px red dashed';
@@ -86,7 +86,7 @@
``` js
Gerrit.install(function(plugin) {
- const domHook = plugin.getDomHook('header-title', {replace: true});
+ const domHook = plugin.hook('header-title', {replace: true});
domHook.onAttached(element => {
element.appendChild(document.createElement('my-site-header'));
});
diff --git a/contrib/abandon_stale.py b/contrib/abandon_stale.py
index fb9eb12..d734cd1 100755
--- a/contrib/abandon_stale.py
+++ b/contrib/abandon_stale.py
@@ -191,7 +191,7 @@
try:
gerrit.post("/changes/" + change_id + "/abandon",
- data='{"message" : "%s"}' % abandon_message)
+ data={"message" : "%s" % abandon_message})
abandoned += 1
except Exception as e:
errors += 1
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 bfa21cb..d409a74 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
@@ -19,6 +19,7 @@
import static com.google.common.truth.TruthJUnit.assume;
import static com.google.gerrit.acceptance.GitUtil.assertPushOk;
import static com.google.gerrit.acceptance.GitUtil.pushHead;
+import static com.google.gerrit.acceptance.PushOneCommit.FILE_CONTENT;
import static com.google.gerrit.acceptance.PushOneCommit.FILE_NAME;
import static com.google.gerrit.acceptance.PushOneCommit.SUBJECT;
import static com.google.gerrit.extensions.client.ReviewerState.CC;
@@ -31,6 +32,7 @@
import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
import static com.google.gerrit.server.project.Util.category;
import static com.google.gerrit.server.project.Util.value;
+import static java.nio.charset.StandardCharsets.UTF_8;
import static java.util.concurrent.TimeUnit.SECONDS;
import static java.util.stream.Collectors.toList;
import static java.util.stream.Collectors.toSet;
@@ -81,6 +83,7 @@
import com.google.gerrit.extensions.common.ChangeInfo;
import com.google.gerrit.extensions.common.ChangeInput;
import com.google.gerrit.extensions.common.ChangeMessageInfo;
+import com.google.gerrit.extensions.common.CommentInfo;
import com.google.gerrit.extensions.common.CommitInfo;
import com.google.gerrit.extensions.common.GitPerson;
import com.google.gerrit.extensions.common.LabelInfo;
@@ -3127,6 +3130,49 @@
gApi.changes().id(r.getChangeId()).setMessage(getCommitMessage(r.getChangeId()));
}
+ @Test
+ public void fourByteEmoji() throws Exception {
+ // U+1F601 GRINNING FACE WITH SMILING EYES
+ String smile = new String(Character.toChars(0x1f601));
+ assertThat(smile).isEqualTo("😁");
+ assertThat(smile).hasLength(2); // Thanks, Java.
+ assertThat(smile.getBytes(UTF_8)).hasLength(4);
+
+ String subject = "A happy change " + smile;
+ PushOneCommit.Result r =
+ pushFactory
+ .create(db, admin.getIdent(), testRepo, subject, FILE_NAME, FILE_CONTENT)
+ .to("refs/for/master");
+ r.assertOkStatus();
+ String id = r.getChangeId();
+
+ ReviewInput ri = ReviewInput.approve();
+ ri.message = "I like it " + smile;
+ ReviewInput.CommentInput ci = new ReviewInput.CommentInput();
+ ci.path = FILE_NAME;
+ ci.side = Side.REVISION;
+ ci.message = "Good " + smile;
+ ri.comments = ImmutableMap.of(FILE_NAME, ImmutableList.of(ci));
+ gApi.changes().id(id).current().review(ri);
+
+ ChangeInfo info =
+ gApi.changes()
+ .id(id)
+ .get(
+ EnumSet.of(
+ ListChangesOption.MESSAGES,
+ ListChangesOption.CURRENT_COMMIT,
+ ListChangesOption.CURRENT_REVISION));
+ assertThat(info.subject).isEqualTo(subject);
+ assertThat(Iterables.getLast(info.messages).message).endsWith(ri.message);
+ assertThat(Iterables.getOnlyElement(info.revisions.values()).commit.message)
+ .startsWith(subject);
+
+ List<CommentInfo> comments =
+ Iterables.getOnlyElement(gApi.changes().id(id).comments().values());
+ assertThat(Iterables.getOnlyElement(comments).message).isEqualTo(ci.message);
+ }
+
private String getCommitMessage(String changeId) throws RestApiException, IOException {
return gApi.changes().id(changeId).current().file("/COMMIT_MSG").content().asString();
}
diff --git a/gerrit-httpd/src/main/resources/com/google/gerrit/httpd/raw/PolyGerritIndexHtml.soy b/gerrit-httpd/src/main/resources/com/google/gerrit/httpd/raw/PolyGerritIndexHtml.soy
index 558cc78..51c60af 100644
--- a/gerrit-httpd/src/main/resources/com/google/gerrit/httpd/raw/PolyGerritIndexHtml.soy
+++ b/gerrit-httpd/src/main/resources/com/google/gerrit/httpd/raw/PolyGerritIndexHtml.soy
@@ -41,6 +41,10 @@
// @see https://github.com/w3c/preload/issues/32 regarding crossorigin
<link rel="preload" href="{$staticResourcePath}/fonts/RobotoMono-Regular.woff2" as="font" type="font/woff2" crossorigin>{\n}
<link rel="preload" href="{$staticResourcePath}/fonts/RobotoMono-Regular.woff" as="font" type="font/woff" crossorigin>{\n}
+ <link rel="preload" href="{$staticResourcePath}/fonts/Roboto-Regular.woff2" as="font" type="font/woff2" crossorigin>{\n}
+ <link rel="preload" href="{$staticResourcePath}/fonts/Roboto-Regular.woff" as="font" type="font/woff" crossorigin>{\n}
+ <link rel="preload" href="{$staticResourcePath}/fonts/Roboto-Medium.woff2" as="font" type="font/woff2" crossorigin>{\n}
+ <link rel="preload" href="{$staticResourcePath}/fonts/Roboto-Medium.woff" as="font" type="font/woff" crossorigin>{\n}
<link rel="stylesheet" href="{$staticResourcePath}/styles/fonts.css">{\n}
<link rel="stylesheet" href="{$staticResourcePath}/styles/main.css">{\n}
<script src="{$staticResourcePath}/bower_components/webcomponentsjs/webcomponents-lite.js"></script>{\n}
diff --git a/gerrit-launcher/src/main/java/com/google/gerrit/launcher/GerritLauncher.java b/gerrit-launcher/src/main/java/com/google/gerrit/launcher/GerritLauncher.java
index 4efbecc..072d1ed 100644
--- a/gerrit-launcher/src/main/java/com/google/gerrit/launcher/GerritLauncher.java
+++ b/gerrit-launcher/src/main/java/com/google/gerrit/launcher/GerritLauncher.java
@@ -619,7 +619,6 @@
// If the build system provides us with a source root, use that.
try (InputStream stream = self.getResourceAsStream(SOURCE_ROOT_RESOURCE)) {
- System.err.println("URL: " + stream);
if (stream != null) {
try (Scanner scan = new Scanner(stream, UTF_8.name()).useDelimiter("\n")) {
if (scan.hasNext()) {
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/RefNames.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/RefNames.java
index f1eef0b..89de9dc 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/RefNames.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/RefNames.java
@@ -102,19 +102,13 @@
}
public static String changeMetaRef(Change.Id id) {
- StringBuilder r = new StringBuilder();
- r.append(REFS_CHANGES);
- r.append(shard(id.get()));
- r.append(META_SUFFIX);
- return r.toString();
+ StringBuilder r = newStringBuilder().append(REFS_CHANGES);
+ return shard(id.get(), r).append(META_SUFFIX).toString();
}
public static String robotCommentsRef(Change.Id id) {
- StringBuilder r = new StringBuilder();
- r.append(REFS_CHANGES);
- r.append(shard(id.get()));
- r.append(ROBOT_COMMENTS_SUFFIX);
- return r.toString();
+ StringBuilder r = newStringBuilder().append(REFS_CHANGES);
+ return shard(id.get(), r).append(ROBOT_COMMENTS_SUFFIX).toString();
}
public static boolean isNoteDbMetaRef(String ref) {
@@ -129,16 +123,12 @@
}
public static String refsUsers(Account.Id accountId) {
- StringBuilder r = new StringBuilder();
- r.append(REFS_USERS);
- r.append(shard(accountId.get()));
- return r.toString();
+ StringBuilder r = newStringBuilder().append(REFS_USERS);
+ return shard(accountId.get(), r).toString();
}
public static String refsDraftComments(Change.Id changeId, Account.Id accountId) {
- StringBuilder r = buildRefsPrefix(REFS_DRAFT_COMMENTS, changeId.get());
- r.append(accountId.get());
- return r.toString();
+ return buildRefsPrefix(REFS_DRAFT_COMMENTS, changeId.get()).append(accountId.get()).toString();
}
public static String refsDraftCommentsPrefix(Change.Id changeId) {
@@ -146,9 +136,7 @@
}
public static String refsStarredChanges(Change.Id changeId, Account.Id accountId) {
- StringBuilder r = buildRefsPrefix(REFS_STARRED_CHANGES, changeId.get());
- r.append(accountId.get());
- return r.toString();
+ return buildRefsPrefix(REFS_STARRED_CHANGES, changeId.get()).append(accountId.get()).toString();
}
public static String refsStarredChangesPrefix(Change.Id changeId) {
@@ -156,11 +144,8 @@
}
private static StringBuilder buildRefsPrefix(String prefix, int id) {
- StringBuilder r = new StringBuilder();
- r.append(prefix);
- r.append(shard(id));
- r.append('/');
- return r;
+ StringBuilder r = newStringBuilder().append(prefix);
+ return shard(id, r).append('/');
}
public static String refsCacheAutomerge(String hash) {
@@ -171,15 +156,18 @@
if (id < 0) {
return null;
}
- StringBuilder r = new StringBuilder();
+ return shard(id, newStringBuilder()).toString();
+ }
+
+ private static StringBuilder shard(int id, StringBuilder sb) {
int n = id % 100;
if (n < 10) {
- r.append('0');
+ sb.append('0');
}
- r.append(n);
- r.append('/');
- r.append(id);
- return r.toString();
+ sb.append(n);
+ sb.append('/');
+ sb.append(id);
+ return sb;
}
/**
@@ -363,5 +351,12 @@
return Integer.valueOf(name.substring(i, name.length()));
}
+ private static StringBuilder newStringBuilder() {
+ // Many refname types in this file are always are longer than the default of 16 chars, so
+ // presize StringBuilders larger by default. This hurts readability less than accurate
+ // calculations would, at a negligible cost to memory overhead.
+ return new StringBuilder(64);
+ }
+
private RefNames() {}
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/ReviewerJson.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/ReviewerJson.java
index 5551320..3928b4f 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/ReviewerJson.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/ReviewerJson.java
@@ -20,7 +20,6 @@
import com.google.common.collect.Lists;
import com.google.gerrit.common.data.LabelType;
import com.google.gerrit.common.data.LabelTypes;
-import com.google.gerrit.common.data.PermissionRange;
import com.google.gerrit.common.data.SubmitRecord;
import com.google.gerrit.extensions.api.changes.ReviewerInfo;
import com.google.gerrit.reviewdb.client.Account;
@@ -126,13 +125,9 @@
// Don't use Maps.newTreeMap(Comparator) due to OpenJDK bug 100167.
out.approvals = new TreeMap<>(labelTypes.nameComparator());
for (PatchSetApproval ca : approvals) {
- for (PermissionRange pr : cd.changeControl().getLabelRanges()) {
- if (!pr.isEmpty()) {
- LabelType at = labelTypes.byLabel(ca.getLabelId());
- if (at != null) {
- out.approvals.put(at.getName(), formatValue(ca.getValue()));
- }
- }
+ LabelType at = labelTypes.byLabel(ca.getLabelId());
+ if (at != null) {
+ out.approvals.put(at.getName(), formatValue(ca.getValue()));
}
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/ChangeControl.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/ChangeControl.java
index 4c38661..f5e4542 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/ChangeControl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/ChangeControl.java
@@ -48,7 +48,6 @@
import java.io.IOException;
import java.util.Collection;
import java.util.EnumSet;
-import java.util.List;
import java.util.Map;
import java.util.Set;
@@ -269,11 +268,6 @@
return canAbandon(db) && refControl.asForRef().testOrFalse(RefPermission.CREATE_CHANGE);
}
- /** All value ranges of any allowed label permission. */
- public List<PermissionRange> getLabelRanges() {
- return getRefControl().getLabelRanges(isOwner());
- }
-
/** The range of permitted values associated with a label permission. */
private PermissionRange getRange(String permission) {
return getRefControl().getRange(permission, isOwner());
@@ -315,7 +309,7 @@
}
/** Is this user the owner of the change? */
- boolean isOwner() {
+ private boolean isOwner() {
if (getUser().isIdentifiedUser()) {
Account.Id id = getUser().asIdentifiedUser().getAccountId();
return id.equals(getChange().getOwner());
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/RefControl.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/RefControl.java
index 5694139..3736360 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/RefControl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/RefControl.java
@@ -323,27 +323,6 @@
return canForcePerform(Permission.EDIT_TOPIC_NAME);
}
- /** All value ranges of any allowed label permission. */
- List<PermissionRange> getLabelRanges(boolean isChangeOwner) {
- List<PermissionRange> r = new ArrayList<>();
- for (Map.Entry<String, List<PermissionRule>> e : relevant.getDeclaredPermissions()) {
- if (Permission.isLabel(e.getKey())) {
- int min = 0;
- int max = 0;
- for (PermissionRule rule : e.getValue()) {
- if (projectControl.match(rule, isChangeOwner)) {
- min = Math.min(min, rule.getMin());
- max = Math.max(max, rule.getMax());
- }
- }
- if (min != 0 || max != 0) {
- r.add(new PermissionRange(e.getKey(), min, max));
- }
- }
- }
- return r;
- }
-
/** The range of permitted values associated with a label permission. */
PermissionRange getRange(String permission) {
return getRange(permission, false);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/RemoveReviewerControl.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/RemoveReviewerControl.java
index 591fcc2..b042183 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/RemoveReviewerControl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/RemoveReviewerControl.java
@@ -87,7 +87,9 @@
}
}
// The change owner may remove any zero or positive score.
- if (changeControl.isOwner() && 0 <= value) {
+ if (currentUser.isIdentifiedUser()
+ && currentUser.getAccountId().equals(notes.getChange().getOwner())
+ && 0 <= value) {
return true;
}
// Users with the remove reviewer permission, the branch owner, project
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/SetAccess.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/SetAccess.java
index 9cba53b..e875388 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/SetAccess.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/SetAccess.java
@@ -14,18 +14,10 @@
package com.google.gerrit.server.project;
-import com.google.common.base.MoreObjects;
import com.google.common.base.Strings;
+import com.google.common.collect.Iterables;
import com.google.gerrit.common.data.AccessSection;
-import com.google.gerrit.common.data.GlobalCapability;
-import com.google.gerrit.common.data.GroupDescription;
-import com.google.gerrit.common.data.GroupReference;
-import com.google.gerrit.common.data.Permission;
-import com.google.gerrit.common.data.PermissionRule;
import com.google.gerrit.common.errors.InvalidNameException;
-import com.google.gerrit.extensions.api.access.AccessSectionInfo;
-import com.google.gerrit.extensions.api.access.PermissionInfo;
-import com.google.gerrit.extensions.api.access.PermissionRuleInfo;
import com.google.gerrit.extensions.api.access.ProjectAccessInfo;
import com.google.gerrit.extensions.api.access.ProjectAccessInput;
import com.google.gerrit.extensions.restapi.AuthException;
@@ -37,10 +29,8 @@
import com.google.gerrit.reviewdb.client.Project;
import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.account.GroupBackend;
-import com.google.gerrit.server.config.AllProjectsName;
import com.google.gerrit.server.git.MetaDataUpdate;
import com.google.gerrit.server.git.ProjectConfig;
-import com.google.gerrit.server.group.GroupsCollection;
import com.google.gerrit.server.permissions.GlobalPermission;
import com.google.gerrit.server.permissions.PermissionBackend;
import com.google.gerrit.server.permissions.PermissionBackendException;
@@ -49,44 +39,35 @@
import com.google.inject.Provider;
import com.google.inject.Singleton;
import java.io.IOException;
-import java.util.ArrayList;
-import java.util.Collections;
import java.util.List;
-import java.util.Map;
import org.eclipse.jgit.errors.ConfigInvalidException;
@Singleton
public class SetAccess implements RestModifyView<ProjectResource, ProjectAccessInput> {
protected final GroupBackend groupBackend;
private final PermissionBackend permissionBackend;
- private final GroupsCollection groupsCollection;
private final Provider<MetaDataUpdate.User> metaDataUpdateFactory;
- private final AllProjectsName allProjects;
- private final Provider<SetParent> setParent;
private final GetAccess getAccess;
private final ProjectCache projectCache;
private final Provider<IdentifiedUser> identifiedUser;
+ private final SetAccessUtil accessUtil;
@Inject
private SetAccess(
GroupBackend groupBackend,
PermissionBackend permissionBackend,
Provider<MetaDataUpdate.User> metaDataUpdateFactory,
- AllProjectsName allProjects,
- Provider<SetParent> setParent,
- GroupsCollection groupsCollection,
ProjectCache projectCache,
GetAccess getAccess,
- Provider<IdentifiedUser> identifiedUser) {
+ Provider<IdentifiedUser> identifiedUser,
+ SetAccessUtil accessUtil) {
this.groupBackend = groupBackend;
this.permissionBackend = permissionBackend;
this.metaDataUpdateFactory = metaDataUpdateFactory;
- this.allProjects = allProjects;
- this.setParent = setParent;
- this.groupsCollection = groupsCollection;
this.getAccess = getAccess;
this.projectCache = projectCache;
this.identifiedUser = identifiedUser;
+ this.accessUtil = accessUtil;
}
@Override
@@ -94,113 +75,39 @@
throws ResourceNotFoundException, ResourceConflictException, IOException, AuthException,
BadRequestException, UnprocessableEntityException, OrmException,
PermissionBackendException {
- List<AccessSection> removals = getAccessSections(input.remove);
- List<AccessSection> additions = getAccessSections(input.add);
MetaDataUpdate.User metaDataUpdateUser = metaDataUpdateFactory.get();
- ProjectControl projectControl = rsrc.getControl();
ProjectConfig config;
- Project.NameKey newParentProjectName =
- input.parent == null ? null : new Project.NameKey(input.parent);
-
+ List<AccessSection> removals = accessUtil.getAccessSections(input.remove);
+ List<AccessSection> additions = accessUtil.getAccessSections(input.add);
try (MetaDataUpdate md = metaDataUpdateUser.create(rsrc.getNameKey())) {
config = ProjectConfig.read(md);
- // Perform removal checks
- for (AccessSection section : removals) {
+ // Check that the user has the right permissions.
+ boolean checkedAdmin = false;
+ for (AccessSection section : Iterables.concat(additions, removals)) {
boolean isGlobalCapabilities = AccessSection.GLOBAL_CAPABILITIES.equals(section.getName());
-
if (isGlobalCapabilities) {
- checkGlobalCapabilityPermissions(config.getName());
- } else if (!projectControl.controlForRef(section.getName()).isOwner()) {
+ if (!checkedAdmin) {
+ permissionBackend.user(identifiedUser).check(GlobalPermission.ADMINISTRATE_SERVER);
+ checkedAdmin = true;
+ }
+ } else if (!rsrc.getControl().controlForRef(section.getName()).isOwner()) {
throw new AuthException(
- "You are not allowed to edit permissionsfor ref: " + section.getName());
- }
- }
- // Perform addition checks
- for (AccessSection section : additions) {
- String name = section.getName();
- boolean isGlobalCapabilities = AccessSection.GLOBAL_CAPABILITIES.equals(name);
-
- if (isGlobalCapabilities) {
- checkGlobalCapabilityPermissions(config.getName());
- } else {
- if (!AccessSection.isValid(name)) {
- throw new BadRequestException("invalid section name");
- }
- if (!projectControl.controlForRef(name).isOwner()) {
- throw new AuthException("You are not allowed to edit permissionsfor ref: " + name);
- }
- RefPattern.validate(name);
- }
-
- // Check all permissions for soundness
- for (Permission p : section.getPermissions()) {
- if (isGlobalCapabilities && !GlobalCapability.isCapability(p.getName())) {
- throw new BadRequestException(
- "Cannot add non-global capability " + p.getName() + " to global capabilities");
- }
+ "You are not allowed to edit permissions for ref: " + section.getName());
}
}
- // Apply removals
- for (AccessSection section : removals) {
- if (section.getPermissions().isEmpty()) {
- // Remove entire section
- config.remove(config.getAccessSection(section.getName()));
- }
- // Remove specific permissions
- for (Permission p : section.getPermissions()) {
- if (p.getRules().isEmpty()) {
- config.remove(config.getAccessSection(section.getName()), p);
- } else {
- for (PermissionRule r : p.getRules()) {
- config.remove(config.getAccessSection(section.getName()), p, r);
- }
- }
- }
- }
+ accessUtil.validateChanges(config, removals, additions);
+ accessUtil.applyChanges(config, removals, additions);
- // Apply additions
- for (AccessSection section : additions) {
- AccessSection currentAccessSection = config.getAccessSection(section.getName());
-
- if (currentAccessSection == null) {
- // Add AccessSection
- config.replace(section);
- } else {
- for (Permission p : section.getPermissions()) {
- Permission currentPermission = currentAccessSection.getPermission(p.getName());
- if (currentPermission == null) {
- // Add Permission
- currentAccessSection.addPermission(p);
- } else {
- for (PermissionRule r : p.getRules()) {
- // AddPermissionRule
- currentPermission.add(r);
- }
- }
- }
- }
- }
-
- if (newParentProjectName != null
- && !config.getProject().getNameKey().equals(allProjects)
- && !config.getProject().getParent(allProjects).equals(newParentProjectName)) {
- try {
- setParent
- .get()
- .validateParentUpdate(
- projectControl.getProject().getNameKey(),
- projectControl.getUser().asIdentifiedUser(),
- MoreObjects.firstNonNull(newParentProjectName, allProjects).get(),
- true);
- } catch (UnprocessableEntityException e) {
- throw new ResourceConflictException(e.getMessage(), e);
- }
- config.getProject().setParentName(newParentProjectName);
- }
+ accessUtil.setParentName(
+ identifiedUser.get(),
+ config,
+ rsrc.getNameKey(),
+ input.parent == null ? null : new Project.NameKey(input.parent),
+ !checkedAdmin);
if (!Strings.isNullOrEmpty(input.message)) {
if (!input.message.endsWith("\n")) {
@@ -221,68 +128,4 @@
return getAccess.apply(rsrc.getNameKey());
}
-
- private List<AccessSection> getAccessSections(Map<String, AccessSectionInfo> sectionInfos)
- throws UnprocessableEntityException {
- if (sectionInfos == null) {
- return Collections.emptyList();
- }
-
- List<AccessSection> sections = new ArrayList<>(sectionInfos.size());
- for (Map.Entry<String, AccessSectionInfo> entry : sectionInfos.entrySet()) {
- AccessSection accessSection = new AccessSection(entry.getKey());
-
- if (entry.getValue().permissions == null) {
- continue;
- }
-
- for (Map.Entry<String, PermissionInfo> permissionEntry :
- entry.getValue().permissions.entrySet()) {
- Permission p = new Permission(permissionEntry.getKey());
- if (permissionEntry.getValue().exclusive != null) {
- p.setExclusiveGroup(permissionEntry.getValue().exclusive);
- }
-
- if (permissionEntry.getValue().rules == null) {
- continue;
- }
- for (Map.Entry<String, PermissionRuleInfo> permissionRuleInfoEntry :
- permissionEntry.getValue().rules.entrySet()) {
- PermissionRuleInfo pri = permissionRuleInfoEntry.getValue();
-
- GroupDescription.Basic group = groupsCollection.parseId(permissionRuleInfoEntry.getKey());
- if (group == null) {
- throw new UnprocessableEntityException(
- permissionRuleInfoEntry.getKey() + " is not a valid group ID");
- }
- PermissionRule r = new PermissionRule(GroupReference.forGroup(group));
- if (pri != null) {
- if (pri.max != null) {
- r.setMax(pri.max);
- }
- if (pri.min != null) {
- r.setMin(pri.min);
- }
- r.setAction(GetAccess.ACTION_TYPE.inverse().get(pri.action));
- if (pri.force != null) {
- r.setForce(pri.force);
- }
- }
- p.add(r);
- }
- accessSection.getPermissions().add(p);
- }
- sections.add(accessSection);
- }
- return sections;
- }
-
- private void checkGlobalCapabilityPermissions(Project.NameKey projectName)
- throws BadRequestException, AuthException, PermissionBackendException {
- if (!allProjects.equals(projectName)) {
- throw new BadRequestException(
- "Cannot edit global capabilities for projects other than " + allProjects.get());
- }
- permissionBackend.user(identifiedUser).check(GlobalPermission.ADMINISTRATE_SERVER);
- }
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/SetAccessUtil.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/SetAccessUtil.java
new file mode 100644
index 0000000..848d68c
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/SetAccessUtil.java
@@ -0,0 +1,224 @@
+// 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.project;
+
+import com.google.common.collect.Iterables;
+import com.google.gerrit.common.data.AccessSection;
+import com.google.gerrit.common.data.GlobalCapability;
+import com.google.gerrit.common.data.GroupDescription;
+import com.google.gerrit.common.data.GroupReference;
+import com.google.gerrit.common.data.Permission;
+import com.google.gerrit.common.data.PermissionRule;
+import com.google.gerrit.common.errors.InvalidNameException;
+import com.google.gerrit.extensions.api.access.AccessSectionInfo;
+import com.google.gerrit.extensions.api.access.PermissionInfo;
+import com.google.gerrit.extensions.api.access.PermissionRuleInfo;
+import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.extensions.restapi.BadRequestException;
+import com.google.gerrit.extensions.restapi.ResourceConflictException;
+import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.config.AllProjectsName;
+import com.google.gerrit.server.git.ProjectConfig;
+import com.google.gerrit.server.group.GroupsCollection;
+import com.google.gerrit.server.permissions.PermissionBackendException;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import com.google.inject.Singleton;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+
+@Singleton
+public class SetAccessUtil {
+ private final GroupsCollection groupsCollection;
+ private final AllProjectsName allProjects;
+ private final Provider<SetParent> setParent;
+
+ @Inject
+ private SetAccessUtil(
+ GroupsCollection groupsCollection,
+ AllProjectsName allProjects,
+ Provider<SetParent> setParent) {
+ this.groupsCollection = groupsCollection;
+ this.allProjects = allProjects;
+ this.setParent = setParent;
+ }
+
+ List<AccessSection> getAccessSections(Map<String, AccessSectionInfo> sectionInfos)
+ throws UnprocessableEntityException {
+ if (sectionInfos == null) {
+ return Collections.emptyList();
+ }
+
+ List<AccessSection> sections = new ArrayList<>(sectionInfos.size());
+ for (Map.Entry<String, AccessSectionInfo> entry : sectionInfos.entrySet()) {
+ if (entry.getValue().permissions == null) {
+ continue;
+ }
+
+ AccessSection accessSection = new AccessSection(entry.getKey());
+ for (Map.Entry<String, PermissionInfo> permissionEntry :
+ entry.getValue().permissions.entrySet()) {
+ if (permissionEntry.getValue().rules == null) {
+ continue;
+ }
+
+ Permission p = new Permission(permissionEntry.getKey());
+ if (permissionEntry.getValue().exclusive != null) {
+ p.setExclusiveGroup(permissionEntry.getValue().exclusive);
+ }
+
+ for (Map.Entry<String, PermissionRuleInfo> permissionRuleInfoEntry :
+ permissionEntry.getValue().rules.entrySet()) {
+ GroupDescription.Basic group = groupsCollection.parseId(permissionRuleInfoEntry.getKey());
+ if (group == null) {
+ throw new UnprocessableEntityException(
+ permissionRuleInfoEntry.getKey() + " is not a valid group ID");
+ }
+
+ PermissionRuleInfo pri = permissionRuleInfoEntry.getValue();
+ PermissionRule r = new PermissionRule(GroupReference.forGroup(group));
+ if (pri != null) {
+ if (pri.max != null) {
+ r.setMax(pri.max);
+ }
+ if (pri.min != null) {
+ r.setMin(pri.min);
+ }
+ r.setAction(GetAccess.ACTION_TYPE.inverse().get(pri.action));
+ if (pri.force != null) {
+ r.setForce(pri.force);
+ }
+ }
+ p.add(r);
+ }
+ accessSection.getPermissions().add(p);
+ }
+ sections.add(accessSection);
+ }
+ return sections;
+ }
+
+ /**
+ * Checks that the removals and additions are logically valid, but doesn't check current user's
+ * permission.
+ */
+ void validateChanges(
+ ProjectConfig config, List<AccessSection> removals, List<AccessSection> additions)
+ throws BadRequestException, InvalidNameException {
+ // Perform permission checks
+ for (AccessSection section : Iterables.concat(additions, removals)) {
+ boolean isGlobalCapabilities = AccessSection.GLOBAL_CAPABILITIES.equals(section.getName());
+ if (isGlobalCapabilities) {
+ if (!allProjects.equals(config.getName())) {
+ throw new BadRequestException(
+ "Cannot edit global capabilities for projects other than " + allProjects.get());
+ }
+ }
+ }
+
+ // Perform addition checks
+ for (AccessSection section : additions) {
+ String name = section.getName();
+ boolean isGlobalCapabilities = AccessSection.GLOBAL_CAPABILITIES.equals(name);
+
+ if (!isGlobalCapabilities) {
+ if (!AccessSection.isValid(name)) {
+ throw new BadRequestException("invalid section name");
+ }
+ RefPattern.validate(name);
+ } else {
+ // Check all permissions for soundness
+ for (Permission p : section.getPermissions()) {
+ if (!GlobalCapability.isCapability(p.getName())) {
+ throw new BadRequestException(
+ "Cannot add non-global capability " + p.getName() + " to global capabilities");
+ }
+ }
+ }
+ }
+ }
+
+ void applyChanges(
+ ProjectConfig config, List<AccessSection> removals, List<AccessSection> additions) {
+ // Apply removals
+ for (AccessSection section : removals) {
+ if (section.getPermissions().isEmpty()) {
+ // Remove entire section
+ config.remove(config.getAccessSection(section.getName()));
+ continue;
+ }
+
+ // Remove specific permissions
+ for (Permission p : section.getPermissions()) {
+ if (p.getRules().isEmpty()) {
+ config.remove(config.getAccessSection(section.getName()), p);
+ } else {
+ for (PermissionRule r : p.getRules()) {
+ config.remove(config.getAccessSection(section.getName()), p, r);
+ }
+ }
+ }
+ }
+
+ // Apply additions
+ for (AccessSection section : additions) {
+ AccessSection currentAccessSection = config.getAccessSection(section.getName());
+
+ if (currentAccessSection == null) {
+ // Add AccessSection
+ config.replace(section);
+ } else {
+ for (Permission p : section.getPermissions()) {
+ Permission currentPermission = currentAccessSection.getPermission(p.getName());
+ if (currentPermission == null) {
+ // Add Permission
+ currentAccessSection.addPermission(p);
+ } else {
+ for (PermissionRule r : p.getRules()) {
+ // AddPermissionRule
+ currentPermission.add(r);
+ }
+ }
+ }
+ }
+ }
+ }
+
+ void setParentName(
+ IdentifiedUser identifiedUser,
+ ProjectConfig config,
+ Project.NameKey projectName,
+ Project.NameKey newParentProjectName,
+ boolean checkAdmin)
+ throws ResourceConflictException, AuthException, PermissionBackendException {
+ if (newParentProjectName != null
+ && !config.getProject().getNameKey().equals(allProjects)
+ && !config.getProject().getParent(allProjects).equals(newParentProjectName)) {
+ try {
+ setParent
+ .get()
+ .validateParentUpdate(
+ projectName, identifiedUser, newParentProjectName.get(), checkAdmin);
+ } catch (UnprocessableEntityException e) {
+ throw new ResourceConflictException(e.getMessage(), e);
+ }
+ config.getProject().setParentName(newParentProjectName);
+ }
+ }
+}
diff --git a/gerrit-util-cli/src/main/java/com/google/gerrit/util/cli/CmdLineParser.java b/gerrit-util-cli/src/main/java/com/google/gerrit/util/cli/CmdLineParser.java
index 0a75bfc..f2d07ed 100644
--- a/gerrit-util-cli/src/main/java/com/google/gerrit/util/cli/CmdLineParser.java
+++ b/gerrit-util-cli/src/main/java/com/google/gerrit/util/cli/CmdLineParser.java
@@ -395,7 +395,7 @@
MyParser(Object bean) {
super(bean);
- parseAdditionalOptions("", bean, new HashSet<>());
+ parseAdditionalOptions(bean, new HashSet<>());
ensureOptionsInitialized();
}
@@ -433,7 +433,7 @@
}
}
- private void parseAdditionalOptions(String prefix, Object bean, Set<Object> parsedBeans) {
+ private void parseAdditionalOptions(Object bean, Set<Object> parsedBeans) {
for (Class<?> c = bean.getClass(); c != null; c = c.getSuperclass()) {
for (Field f : c.getDeclaredFields()) {
if (f.isAnnotationPresent(Options.class)) {
@@ -443,8 +443,7 @@
} catch (IllegalAccessException e) {
throw new IllegalAnnotationError(e);
}
- parseWithPrefix(
- prefix + f.getAnnotation(Options.class).prefix(), additionalBean, parsedBeans);
+ parseWithPrefix(f.getAnnotation(Options.class).prefix(), additionalBean, parsedBeans);
}
}
}
diff --git a/lib/fonts/BUILD b/lib/fonts/BUILD
index 57429f3..025b93e 100644
--- a/lib/fonts/BUILD
+++ b/lib/fonts/BUILD
@@ -3,8 +3,12 @@
# Roboto Mono. Version 2.136
# https://github.com/google/roboto/releases/tag/v2.136
filegroup(
- name = "robotomono",
+ name = "robotofonts",
srcs = [
+ "Roboto-Medium.woff",
+ "Roboto-Medium.woff2",
+ "Roboto-Regular.woff",
+ "Roboto-Regular.woff2",
"RobotoMono-Regular.woff",
"RobotoMono-Regular.woff2",
],
diff --git a/lib/fonts/Roboto-Medium.woff b/lib/fonts/Roboto-Medium.woff
new file mode 100644
index 0000000..720bd3e
--- /dev/null
+++ b/lib/fonts/Roboto-Medium.woff
Binary files differ
diff --git a/lib/fonts/Roboto-Medium.woff2 b/lib/fonts/Roboto-Medium.woff2
new file mode 100644
index 0000000..c003fba
--- /dev/null
+++ b/lib/fonts/Roboto-Medium.woff2
Binary files differ
diff --git a/lib/fonts/Roboto-Regular.woff b/lib/fonts/Roboto-Regular.woff
new file mode 100644
index 0000000..03e84eb
--- /dev/null
+++ b/lib/fonts/Roboto-Regular.woff
Binary files differ
diff --git a/lib/fonts/Roboto-Regular.woff2 b/lib/fonts/Roboto-Regular.woff2
new file mode 100644
index 0000000..6fa4939
--- /dev/null
+++ b/lib/fonts/Roboto-Regular.woff2
Binary files differ
diff --git a/plugins/replication b/plugins/replication
index 297b749..643d635 160000
--- a/plugins/replication
+++ b/plugins/replication
@@ -1 +1 @@
-Subproject commit 297b749038153527291b43cb08b162eb475adcd7
+Subproject commit 643d635a4502e2a2df6cb02edade88bad3fd953a
diff --git a/polygerrit-ui/BUILD b/polygerrit-ui/BUILD
index d4d2322..31ab6aa 100644
--- a/polygerrit-ui/BUILD
+++ b/polygerrit-ui/BUILD
@@ -29,7 +29,7 @@
genrule2(
name = "fonts",
srcs = [
- "//lib/fonts:robotomono",
+ "//lib/fonts:robotofonts",
],
outs = ["fonts.zip"],
cmd = " && ".join([
diff --git a/polygerrit-ui/app/elements/admin/gr-admin-group-list/gr-admin-group-list.js b/polygerrit-ui/app/elements/admin/gr-admin-group-list/gr-admin-group-list.js
index 00aeb91..72439e2 100644
--- a/polygerrit-ui/app/elements/admin/gr-admin-group-list/gr-admin-group-list.js
+++ b/polygerrit-ui/app/elements/admin/gr-admin-group-list/gr-admin-group-list.js
@@ -70,6 +70,7 @@
attached() {
this._getCreateGroupCapability();
this.fire('title-change', {title: 'Groups'});
+ this._maybeOpenCreateOverlay(this.params);
},
_paramsChanged(params) {
@@ -81,6 +82,16 @@
this._offset);
},
+ /**
+ * Opens the create overlay if the route has a hash 'create'
+ * @param {!Object} params
+ */
+ _maybeOpenCreateOverlay(params) {
+ if (params && params.openCreateModal) {
+ this.$.createOverlay.open();
+ }
+ },
+
_computeGroupUrl(id) {
return this.getUrl(this._path + '/', id);
},
diff --git a/polygerrit-ui/app/elements/admin/gr-admin-group-list/gr-admin-group-list_test.html b/polygerrit-ui/app/elements/admin/gr-admin-group-list/gr-admin-group-list_test.html
index 4cc1afe..06428c7 100644
--- a/polygerrit-ui/app/elements/admin/gr-admin-group-list/gr-admin-group-list_test.html
+++ b/polygerrit-ui/app/elements/admin/gr-admin-group-list/gr-admin-group-list_test.html
@@ -90,6 +90,18 @@
test('_shownGroups', () => {
assert.equal(element._shownGroups.length, 25);
});
+
+ test('_maybeOpenCreateOverlay', () => {
+ const overlayOpen = sandbox.stub(element.$.createOverlay, 'open');
+ element._maybeOpenCreateOverlay();
+ assert.isFalse(overlayOpen.called);
+ const params = {};
+ element._maybeOpenCreateOverlay(params);
+ assert.isFalse(overlayOpen.called);
+ params.openCreateModal = true;
+ element._maybeOpenCreateOverlay(params);
+ assert.isTrue(overlayOpen.called);
+ });
});
suite('test with less then 25 groups', () => {
diff --git a/polygerrit-ui/app/elements/admin/gr-project-list/gr-project-list.js b/polygerrit-ui/app/elements/admin/gr-project-list/gr-project-list.js
index 3b568ca..070cc2f 100644
--- a/polygerrit-ui/app/elements/admin/gr-project-list/gr-project-list.js
+++ b/polygerrit-ui/app/elements/admin/gr-project-list/gr-project-list.js
@@ -70,6 +70,7 @@
attached() {
this._getCreateProjectCapability();
this.fire('title-change', {title: 'Projects'});
+ this._maybeOpenCreateOverlay(this.params);
},
_paramsChanged(params) {
@@ -81,6 +82,16 @@
this._offset);
},
+ /**
+ * Opens the create overlay if the route has a hash 'create'
+ * @param {!Object} params
+ */
+ _maybeOpenCreateOverlay(params) {
+ if (params && params.openCreateModal) {
+ this.$.createOverlay.open();
+ }
+ },
+
_computeProjectUrl(name) {
return this.getUrl(this._path + '/', name);
},
diff --git a/polygerrit-ui/app/elements/admin/gr-project-list/gr-project-list_test.html b/polygerrit-ui/app/elements/admin/gr-project-list/gr-project-list_test.html
index 117df3a..87732b8 100644
--- a/polygerrit-ui/app/elements/admin/gr-project-list/gr-project-list_test.html
+++ b/polygerrit-ui/app/elements/admin/gr-project-list/gr-project-list_test.html
@@ -66,13 +66,11 @@
suite('list with projects', () => {
setup(done => {
projects = _.times(26, projectGenerator);
-
stub('gr-rest-api-interface', {
getProjects(num, offset) {
return Promise.resolve(projects);
},
});
-
element._paramsChanged(value).then(() => { flush(done); });
});
@@ -86,6 +84,18 @@
test('_shownProjects', () => {
assert.equal(element._shownProjects.length, 25);
});
+
+ test('_maybeOpenCreateOverlay', () => {
+ const overlayOpen = sandbox.stub(element.$.createOverlay, 'open');
+ element._maybeOpenCreateOverlay();
+ assert.isFalse(overlayOpen.called);
+ const params = {};
+ element._maybeOpenCreateOverlay(params);
+ assert.isFalse(overlayOpen.called);
+ params.openCreateModal = true;
+ element._maybeOpenCreateOverlay(params);
+ assert.isTrue(overlayOpen.called);
+ });
});
suite('list with less then 25 projects', () => {
diff --git a/polygerrit-ui/app/elements/change/gr-download-dialog/gr-download-dialog.html b/polygerrit-ui/app/elements/change/gr-download-dialog/gr-download-dialog.html
index 1d07cfe..fdd9b26 100644
--- a/polygerrit-ui/app/elements/change/gr-download-dialog/gr-download-dialog.html
+++ b/polygerrit-ui/app/elements/change/gr-download-dialog/gr-download-dialog.html
@@ -77,10 +77,15 @@
<div class="patchFiles">
<label>Patch file</label>
<div>
- <a id="download" href$="[[_computeDownloadLink(change, patchNum)]]">
+ <a
+ id="download"
+ href$="[[_computeDownloadLink(change, patchNum)]]"
+ download>
[[_computeDownloadFilename(change, patchNum)]]
</a>
- <a href$="[[_computeZipDownloadLink(change, patchNum)]]">
+ <a
+ href$="[[_computeZipDownloadLink(change, patchNum)]]"
+ download>
[[_computeZipDownloadFilename(change, patchNum)]]
</a>
</div>
@@ -89,7 +94,9 @@
<label>Archive</label>
<div id="archives" class="archives">
<template is="dom-repeat" items="[[config.archives]]" as="format">
- <a href$="[[_computeArchiveDownloadLink(change, patchNum, format)]]">
+ <a
+ href$="[[_computeArchiveDownloadLink(change, patchNum, format)]]"
+ download>
[[format]]
</a>
</template>
diff --git a/polygerrit-ui/app/elements/change/gr-download-dialog/gr-download-dialog_test.html b/polygerrit-ui/app/elements/change/gr-download-dialog/gr-download-dialog_test.html
index b52363b..f6e1748 100644
--- a/polygerrit-ui/app/elements/change/gr-download-dialog/gr-download-dialog_test.html
+++ b/polygerrit-ui/app/elements/change/gr-download-dialog/gr-download-dialog_test.html
@@ -115,31 +115,39 @@
let sandbox;
setup(() => {
- element = fixture('basic');
sandbox = sinon.sandbox.create();
+
+ element = fixture('basic');
+ element.patchNum = '1';
+ element.config = {
+ schemes: {
+ 'anonymous http': {},
+ 'http': {},
+ 'repo': {},
+ 'ssh': {},
+ },
+ archives: ['tgz', 'tar', 'tbz2', 'txz'],
+ };
+
+ flushAsynchronousOperations();
});
teardown(() => {
sandbox.restore();
});
+ test('anchors use download attribute', () => {
+ const anchors = Polymer.dom(element.root).querySelectorAll('a');
+ assert.isTrue(!anchors.some(a => !a.hasAttribute('download')));
+ });
+
suite('gr-download-dialog tests with no fetch options', () => {
setup(() => {
element.change = getChangeObjectNoFetch();
- element.patchNum = '1';
- element.config = {
- schemes: {
- 'anonymous http': {},
- 'http': {},
- 'repo': {},
- 'ssh': {},
- },
- archives: ['tgz', 'tar', 'tbz2', 'txz'],
- };
+ flushAsynchronousOperations();
});
test('focuses on first download link if no copy links', () => {
- flushAsynchronousOperations();
const focusStub = sandbox.stub(element.$.download, 'focus');
element.focus();
assert.isTrue(focusStub.called);
@@ -150,20 +158,10 @@
suite('gr-download-dialog with fetch options', () => {
setup(() => {
element.change = getChangeObject();
- element.patchNum = '1';
- element.config = {
- schemes: {
- 'anonymous http': {},
- 'http': {},
- 'repo': {},
- 'ssh': {},
- },
- archives: ['tgz', 'tar', 'tbz2', 'txz'],
- };
+ flushAsynchronousOperations();
});
test('focuses on first copy link', () => {
- flushAsynchronousOperations();
const focusStub = sinon.stub(element.$.downloadCommands, 'focusOnCopy');
element.focus();
flushAsynchronousOperations();
diff --git a/polygerrit-ui/app/elements/core/gr-account-dropdown/gr-account-dropdown.js b/polygerrit-ui/app/elements/core/gr-account-dropdown/gr-account-dropdown.js
index c147456..e95bd41 100644
--- a/polygerrit-ui/app/elements/core/gr-account-dropdown/gr-account-dropdown.js
+++ b/polygerrit-ui/app/elements/core/gr-account-dropdown/gr-account-dropdown.js
@@ -66,7 +66,7 @@
if (switchAccountUrl) {
const replacements = {path};
const url = this._interpolateUrl(switchAccountUrl, replacements);
- links.push({name: 'Switch account', url});
+ links.push({name: 'Switch account', url, external: true});
}
links.push({name: 'Sign out', url: '/logout'});
return links;
diff --git a/polygerrit-ui/app/elements/core/gr-account-dropdown/gr-account-dropdown_test.html b/polygerrit-ui/app/elements/core/gr-account-dropdown/gr-account-dropdown_test.html
index 6017cb9..1183d9c 100644
--- a/polygerrit-ui/app/elements/core/gr-account-dropdown/gr-account-dropdown_test.html
+++ b/polygerrit-ui/app/elements/core/gr-account-dropdown/gr-account-dropdown_test.html
@@ -83,14 +83,20 @@
// Unparameterized switch account link.
let links = element._getLinks('/switch-account');
assert.equal(links.length, 3);
- assert.deepEqual(links[1],
- {name: 'Switch account', url: '/switch-account'});
+ assert.deepEqual(links[1], {
+ name: 'Switch account',
+ url: '/switch-account',
+ external: true,
+ });
// Parameterized switch account link.
links = element._getLinks('/switch-account${path}', '/c/123');
assert.equal(links.length, 3);
- assert.deepEqual(links[1],
- {name: 'Switch account', url: '/switch-account/c/123'});
+ assert.deepEqual(links[1], {
+ name: 'Switch account',
+ url: '/switch-account/c/123',
+ external: true,
+ });
});
test('_interpolateUrl', () => {
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 c1a5978..5523032 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
@@ -78,6 +78,9 @@
flex: 1;
justify-content: flex-end;
}
+ .rightItems gr-endpoint-decorator:not(:empty) {
+ margin-left: 1em;
+ }
gr-search-bar {
flex-grow: 1;
margin-left: .5em;
@@ -121,6 +124,7 @@
}
gr-search-bar,
.browse,
+ .rightItems .hideOnMobile,
.links > li.hideOnMobile {
display: none;
}
@@ -159,6 +163,9 @@
</ul>
<div class="rightItems">
<gr-search-bar value="{{searchQuery}}" role="search"></gr-search-bar>
+ <gr-endpoint-decorator
+ class="hideOnMobile"
+ name="header-browse-source"></gr-endpoint-decorator>
<div class="accountContainer" id="accountContainer">
<a class="loginButton" href$="[[_loginURL]]">Sign in</a>
<gr-account-dropdown account="[[_account]]"></gr-account-dropdown>
diff --git a/polygerrit-ui/app/elements/core/gr-router/gr-router.js b/polygerrit-ui/app/elements/core/gr-router/gr-router.js
index 1ce3c23..2de74e1 100644
--- a/polygerrit-ui/app/elements/core/gr-router/gr-router.js
+++ b/polygerrit-ui/app/elements/core/gr-router/gr-router.js
@@ -46,6 +46,12 @@
GROUP_LIST_FILTER: '/admin/groups/q/filter::filter',
GROUP_LIST_FILTER_OFFSET: '/admin/groups/q/filter::filter,:offset',
+ // Matches /admin/create-project
+ LEGACY_CREATE_PROJECT: /^\/admin\/create-project\/?$/,
+
+ // Matches /admin/create-project
+ LEGACY_CREATE_GROUP: /^\/admin\/create-group\/?$/,
+
// Matches /admin/projects/<project>
PROJECT: /^\/admin\/projects\/([^,]+)$/,
@@ -72,6 +78,8 @@
TAG_LIST_FILTER_OFFSET:
'/admin/projects/:project,tags/q/filter::filter,:offset',
+ PLUGINS: /^\/plugins\/(.+)$/,
+
PLUGIN_LIST: /^\/admin\/plugins(\/)?$/,
// Matches /admin/plugins[,<offset>][/].
@@ -463,6 +471,12 @@
this._mapRoute(RoutePattern.TAG_LIST_FILTER,
'_handleTagListFilterRoute');
+ this._mapRoute(RoutePattern.LEGACY_CREATE_GROUP,
+ '_handleCreateGroupRoute', true);
+
+ this._mapRoute(RoutePattern.LEGACY_CREATE_PROJECT,
+ '_handleCreateProjectRoute', true);
+
this._mapRoute(RoutePattern.PROJECT_LIST_OFFSET,
'_handleProjectListOffsetRoute');
@@ -474,6 +488,8 @@
this._mapRoute(RoutePattern.PROJECT, '_handleProjectRoute');
+ this._mapRoute(RoutePattern.PLUGINS, '_handlePassThroughRoute');
+
this._mapRoute(RoutePattern.PLUGIN_LIST_OFFSET,
'_handlePluginListOffsetRoute', true);
@@ -612,6 +628,7 @@
adminView: 'gr-admin-group-list',
offset: data.params[1] || 0,
filter: null,
+ openCreateModal: data.hash === 'create',
});
},
@@ -728,6 +745,7 @@
adminView: 'gr-project-list',
offset: data.params[1] || 0,
filter: null,
+ openCreateModal: data.hash === 'create',
});
},
@@ -748,6 +766,18 @@
});
},
+ _handleCreateProjectRoute(data) {
+ // Redirects the legacy route to the new route, which displays the project
+ // list with a hash 'create'.
+ this._redirect('/admin/projects#create');
+ },
+
+ _handleCreateGroupRoute(data) {
+ // Redirects the legacy route to the new route, which displays the group
+ // list with a hash 'create'.
+ this._redirect('/admin/groups#create');
+ },
+
_handleProjectRoute(data) {
this._setParams({
view: Gerrit.Nav.View.ADMIN,
diff --git a/polygerrit-ui/app/elements/core/gr-router/gr-router_test.html b/polygerrit-ui/app/elements/core/gr-router/gr-router_test.html
index c32f6d4..831b905 100644
--- a/polygerrit-ui/app/elements/core/gr-router/gr-router_test.html
+++ b/polygerrit-ui/app/elements/core/gr-router/gr-router_test.html
@@ -125,6 +125,8 @@
const shouldRequireAutoAuth = [
'_handleAdminPlaceholderRoute',
'_handleAgreementsRoute',
+ '_handleCreateGroupRoute',
+ '_handleCreateProjectRoute',
'_handleDiffEditRoute',
'_handleGroupAuditLogRoute',
'_handleGroupInfoRoute',
@@ -641,6 +643,7 @@
adminView: 'gr-admin-group-list',
offset: 0,
filter: null,
+ openCreateModal: false,
});
data.params[1] = 42;
@@ -649,6 +652,16 @@
adminView: 'gr-admin-group-list',
offset: 42,
filter: null,
+ openCreateModal: false,
+ });
+
+ data.hash = 'create';
+ assertDataToParams(data, '_handleGroupListOffsetRoute', {
+ view: Gerrit.Nav.View.ADMIN,
+ adminView: 'gr-admin-group-list',
+ offset: 42,
+ filter: null,
+ openCreateModal: true,
});
});
@@ -812,6 +825,7 @@
adminView: 'gr-project-list',
offset: 0,
filter: null,
+ openCreateModal: false,
});
data.params[1] = 42;
@@ -820,6 +834,16 @@
adminView: 'gr-project-list',
offset: 42,
filter: null,
+ openCreateModal: false,
+ });
+
+ data.hash = 'create';
+ assertDataToParams(data, '_handleProjectListOffsetRoute', {
+ view: Gerrit.Nav.View.ADMIN,
+ adminView: 'gr-project-list',
+ offset: 42,
+ filter: null,
+ openCreateModal: true,
});
});
diff --git a/polygerrit-ui/app/elements/gr-app.html b/polygerrit-ui/app/elements/gr-app.html
index defbe8a..406a4a7 100644
--- a/polygerrit-ui/app/elements/gr-app.html
+++ b/polygerrit-ui/app/elements/gr-app.html
@@ -46,6 +46,7 @@
<link rel="import" href="./core/gr-reporting/gr-reporting.html">
<link rel="import" href="./core/gr-router/gr-router.html">
<link rel="import" href="./diff/gr-diff-view/gr-diff-view.html">
+<link rel="import" href="./plugins/gr-endpoint-decorator/gr-endpoint-decorator.html">
<link rel="import" href="./plugins/gr-external-style/gr-external-style.html">
<link rel="import" href="./plugins/gr-plugin-host/gr-plugin-host.html">
<link rel="import" href="./settings/gr-cla-view/gr-cla-view.html">
@@ -201,6 +202,7 @@
on-close="_handleRegistrationDialogClose">
</gr-registration-dialog>
</gr-overlay>
+ <gr-endpoint-decorator name="plugin-overlay"></gr-endpoint-decorator>
<gr-error-manager id="errorManager"></gr-error-manager>
<gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
<gr-reporting id="reporting"></gr-reporting>
diff --git a/polygerrit-ui/app/elements/plugins/gr-dom-hooks/gr-dom-hooks.js b/polygerrit-ui/app/elements/plugins/gr-dom-hooks/gr-dom-hooks.js
index 02a2085..889333b 100644
--- a/polygerrit-ui/app/elements/plugins/gr-dom-hooks/gr-dom-hooks.js
+++ b/polygerrit-ui/app/elements/plugins/gr-dom-hooks/gr-dom-hooks.js
@@ -14,49 +14,122 @@
(function(window) {
'use strict';
- function GrDomHooks(plugin) {
+ function GrDomHooksManager(plugin) {
this._plugin = plugin;
this._hooks = {};
}
- GrDomHooks.prototype._getName = function(endpointName) {
- return this._plugin.getPluginName() + '-autogenerated-' + endpointName;
+ GrDomHooksManager.prototype._getHookName = function(endpointName,
+ opt_moduleName) {
+ if (opt_moduleName) {
+ return endpointName + ' ' + opt_moduleName;
+ } else {
+ return this._plugin.getPluginName() + '-autogenerated-' + endpointName;
+ }
};
- GrDomHooks.prototype.getDomHook = function(endpointName) {
- const hookName = this._getName(endpointName);
+ GrDomHooksManager.prototype.getDomHook = function(endpointName,
+ opt_moduleName) {
+ const hookName = this._getHookName(endpointName, opt_moduleName);
if (!this._hooks[hookName]) {
- this._hooks[hookName] = new GrDomHook(hookName);
+ this._hooks[hookName] = new GrDomHook(hookName, opt_moduleName);
}
return this._hooks[hookName];
};
- function GrDomHook(hookName) {
+ function GrDomHook(hookName, opt_moduleName) {
+ this._instances = [];
this._callbacks = [];
- // Expose to closure.
- const callbacks = this._callbacks;
- this._componentClass = Polymer({
+ if (opt_moduleName) {
+ this._moduleName = opt_moduleName;
+ } else {
+ this._moduleName = hookName;
+ this._createPlaceholder(hookName);
+ }
+ }
+
+ GrDomHook.prototype._createPlaceholder = function(hookName) {
+ Polymer({
is: hookName,
properties: {
plugin: Object,
content: Object,
},
- attached() {
- callbacks.forEach(callback => {
- callback(this);
- });
- },
});
- }
+ };
+ GrDomHook.prototype.handleInstanceDetached = function(instance) {
+ const index = this._instances.indexOf(instance);
+ if (index !== -1) {
+ this._instances.splice(index, 1);
+ }
+ };
+
+ GrDomHook.prototype.handleInstanceAttached = function(instance) {
+ this._instances.push(instance);
+ this._callbacks.forEach(callback => callback(instance));
+ };
+
+ /**
+ * Get instance of last DOM hook element attached into the endpoint.
+ * Returns a Promise, that's resolved when attachment is done.
+ * @return {!Promise<!Element>}
+ */
+ GrDomHook.prototype.getLastAttached = function() {
+ if (this._instances.length) {
+ return Promise.resolve(this._instances.slice(-1)[0]);
+ }
+ if (!this._lastAttachedPromise) {
+ let resolve;
+ const promise = new Promise(r => resolve = r);
+ this._callbacks.push(resolve);
+ this._lastAttachedPromise = promise.then(element => {
+ this._lastAttachedPromise = null;
+ const index = this._callbacks.indexOf(resolve);
+ if (index !== -1) {
+ this._callbacks.splice(index, 1);
+ }
+ return element;
+ });
+ }
+ return this._lastAttachedPromise;
+ };
+
+ /**
+ * Get all DOM hook elements.
+ */
+ GrDomHook.prototype.getAllAttached = function() {
+ return this._instances;
+ };
+
+ /**
+ * Install a new callback to invoke when a new instance of DOM hook element
+ * is attached.
+ * @param {function(Element)} callback
+ */
GrDomHook.prototype.onAttached = function(callback) {
this._callbacks.push(callback);
return this;
};
+ /**
+ * Name of DOM hook element that will be installed into the endpoint.
+ */
GrDomHook.prototype.getModuleName = function() {
- return this._componentClass.prototype.is;
+ return this._moduleName;
};
- window.GrDomHooks = GrDomHooks;
+ GrDomHook.prototype.getPublicAPI = function() {
+ const result = {};
+ const exposedMethods = [
+ 'onAttached', 'getLastAttached', 'getAllAttached', 'getModuleName',
+ ];
+ for (const p of exposedMethods) {
+ result[p] = this[p].bind(this);
+ }
+ return result;
+ };
+
+ window.GrDomHook = GrDomHook;
+ window.GrDomHooksManager = GrDomHooksManager;
})(window);
diff --git a/polygerrit-ui/app/elements/plugins/gr-dom-hooks/gr-dom-hooks_test.html b/polygerrit-ui/app/elements/plugins/gr-dom-hooks/gr-dom-hooks_test.html
index f92f5c5..f5a7f6f 100644
--- a/polygerrit-ui/app/elements/plugins/gr-dom-hooks/gr-dom-hooks_test.html
+++ b/polygerrit-ui/app/elements/plugins/gr-dom-hooks/gr-dom-hooks_test.html
@@ -33,28 +33,110 @@
<script>
suite('gr-dom-hooks tests', () => {
+ const PUBLIC_METHODS =
+ ['onAttached', 'getLastAttached', 'getAllAttached', 'getModuleName'];
+
let instance;
let sandbox;
+ let hook;
+ let hookInternal;
setup(() => {
sandbox = sinon.sandbox.create();
let plugin;
Gerrit.install(p => { plugin = p; }, '0.1',
'http://test.com/plugins/testplugin/static/test.js');
- instance = new GrDomHooks(plugin);
+ instance = new GrDomHooksManager(plugin);
});
teardown(() => {
sandbox.restore();
});
- test('defines a Polymer components', () => {
- const onAttachedSpy = sandbox.spy();
- instance.getDomHook('foo-bar').onAttached(onAttachedSpy);
- const hookName = Object.keys(instance._hooks).pop();
- assert.equal(hookName, 'testplugin-autogenerated-foo-bar');
- const el = fixture('basic').appendChild(document.createElement(hookName));
- assert.isTrue(onAttachedSpy.calledWithExactly(el));
+ suite('placeholder', () => {
+ setup(()=>{
+ sandbox.stub(GrDomHook.prototype, '_createPlaceholder');
+ hookInternal = instance.getDomHook('foo-bar');
+ hook = hookInternal.getPublicAPI();
+ });
+
+ test('public hook API has only public methods', () => {
+ assert.deepEqual(Object.keys(hook), PUBLIC_METHODS);
+ });
+
+ test('registers placeholder class', () => {
+ assert.isTrue(hookInternal._createPlaceholder.calledWithExactly(
+ 'testplugin-autogenerated-foo-bar'));
+ });
+
+ test('getModuleName()', () => {
+ const hookName = Object.keys(instance._hooks).pop();
+ assert.equal(hookName, 'testplugin-autogenerated-foo-bar');
+ assert.equal(hook.getModuleName(), 'testplugin-autogenerated-foo-bar');
+ });
+ });
+
+ suite('custom element', () => {
+ setup(() => {
+ hookInternal = instance.getDomHook('foo-bar', 'my-el');
+ hook = hookInternal.getPublicAPI();
+ });
+
+ test('public hook API has only public methods', () => {
+ assert.deepEqual(Object.keys(hook), PUBLIC_METHODS);
+ });
+
+ test('getModuleName()', () => {
+ const hookName = Object.keys(instance._hooks).pop();
+ assert.equal(hookName, 'foo-bar my-el');
+ assert.equal(hook.getModuleName(), 'my-el');
+ });
+
+ test('onAttached', () => {
+ const onAttachedSpy = sandbox.spy();
+ hook.onAttached(onAttachedSpy);
+ const [el1, el2] = [
+ document.createElement(hook.getModuleName()),
+ document.createElement(hook.getModuleName()),
+ ];
+ hookInternal.handleInstanceAttached(el1);
+ hookInternal.handleInstanceAttached(el2);
+ assert.isTrue(onAttachedSpy.firstCall.calledWithExactly(el1));
+ assert.isTrue(onAttachedSpy.secondCall.calledWithExactly(el2));
+ });
+
+ test('getAllAttached', () => {
+ const [el1, el2] = [
+ document.createElement(hook.getModuleName()),
+ document.createElement(hook.getModuleName()),
+ ];
+ el1.textContent = 'one';
+ el2.textContent = 'two';
+ hookInternal.handleInstanceAttached(el1);
+ hookInternal.handleInstanceAttached(el2);
+ assert.deepEqual([el1, el2], hook.getAllAttached());
+ hookI.handleInstanceDetached(el1);
+ assert.deepEqual([el2], hook.getAllAttached());
+ });
+
+ test('getLastAttached', () => {
+ const beforeAttachedPromise = hook.getLastAttached().then(
+ el => assert.strictEqual(el1, el));
+ const [el1, el2] = [
+ document.createElement(hook.getModuleName()),
+ document.createElement(hook.getModuleName()),
+ ];
+ el1.textContent = 'one';
+ el2.textContent = 'two';
+ hookInternal.handleInstanceAttached(el1);
+ hookInternal.handleInstanceAttached(el2);
+ const afterAttachedPromise = hook.getLastAttached().then(
+ el => assert.strictEqual(el2, el));
+ return Promise.all([
+ beforeAttachedPromise,
+ afterAttachedPromise,
+ ]);
+ });
});
});
</script>
diff --git a/polygerrit-ui/app/elements/plugins/gr-endpoint-decorator/gr-endpoint-decorator.html b/polygerrit-ui/app/elements/plugins/gr-endpoint-decorator/gr-endpoint-decorator.html
index 49424a1..0928534 100644
--- a/polygerrit-ui/app/elements/plugins/gr-endpoint-decorator/gr-endpoint-decorator.html
+++ b/polygerrit-ui/app/elements/plugins/gr-endpoint-decorator/gr-endpoint-decorator.html
@@ -18,7 +18,7 @@
<link rel="import" href="../../shared/gr-js-api-interface/gr-js-api-interface.html">
<dom-module id="gr-endpoint-decorator">
- <template>
+ <template strip-whitespace>
<content></content>
</template>
<script src="gr-endpoint-decorator.js"></script>
diff --git a/polygerrit-ui/app/elements/plugins/gr-endpoint-decorator/gr-endpoint-decorator.js b/polygerrit-ui/app/elements/plugins/gr-endpoint-decorator/gr-endpoint-decorator.js
index 7e74494..bcd2378 100644
--- a/polygerrit-ui/app/elements/plugins/gr-endpoint-decorator/gr-endpoint-decorator.js
+++ b/polygerrit-ui/app/elements/plugins/gr-endpoint-decorator/gr-endpoint-decorator.js
@@ -19,6 +19,17 @@
properties: {
name: String,
+ /** @type {!Map} */
+ _domHooks: {
+ type: Map,
+ value() { return new Map(); },
+ },
+ },
+
+ detached() {
+ for (const [el, domHook] of this._domHooks) {
+ domHook.handleInstanceDetached(el);
+ }
},
_import(url) {
@@ -31,33 +42,48 @@
const el = document.createElement(name);
el.plugin = plugin;
el.content = this.getContentChildren()[0];
- return Polymer.dom(this.root).appendChild(el);
+ this._appendChild(el);
+ return el;
},
_initReplacement(name, plugin) {
this.getContentChildren().forEach(node => node.remove());
const el = document.createElement(name);
el.plugin = plugin;
- return Polymer.dom(this.root).appendChild(el);
+ this._appendChild(el);
+ return el;
+ },
+
+ _appendChild(el) {
+ Polymer.dom(this.root).appendChild(el);
+ },
+
+ _initModule({moduleName, plugin, type, domHook}) {
+ let el;
+ switch (type) {
+ case 'decorate':
+ el = this._initDecoration(moduleName, plugin);
+ break;
+ case 'replace':
+ el = this._initReplacement(moduleName, plugin);
+ break;
+ }
+ if (el) {
+ domHook.handleInstanceAttached(el);
+ }
+ this._domHooks.set(el, domHook);
},
ready() {
+ Gerrit._endpoints.onNewEndpoint(this.name, this._initModule.bind(this));
Gerrit.awaitPluginsLoaded().then(() => Promise.all(
Gerrit._endpoints.getPlugins(this.name).map(
pluginUrl => this._import(pluginUrl)))
- ).then(() => {
- const modulesData = Gerrit._endpoints.getDetails(this.name);
- for (const {moduleName, plugin, type} of modulesData) {
- switch (type) {
- case 'decorate':
- this._initDecoration(moduleName, plugin);
- break;
- case 'replace':
- this._initReplacement(moduleName, plugin);
- break;
- }
- }
- });
+ ).then(() =>
+ Gerrit._endpoints
+ .getDetails(this.name)
+ .forEach(this._initModule, this)
+ );
},
});
})();
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 8b96dee..e7d1930 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
@@ -34,10 +34,20 @@
let sandbox;
let element;
let plugin;
+ let domHookStub;
setup(done => {
+ Gerrit._endpoints = new GrPluginEndpoints();
+
sandbox = sinon.sandbox.create();
+ domHookStub = {
+ handleInstanceAttached: sandbox.stub(),
+ handleInstanceDetached: sandbox.stub(),
+ };
+ sandbox.stub(
+ GrDomHooksManager.prototype, 'getDomHook').returns(domHookStub);
+
// NB: Order is important.
Gerrit.install(p => {
plugin = p;
@@ -45,11 +55,12 @@
plugin.registerCustomComponent('foo', 'other-module', {replace: true});
}, '0.1', 'http://some/plugin/url.html');
+ sandbox.stub(Gerrit, '_arePluginsLoaded').returns(true);
sandbox.stub(Gerrit, 'awaitPluginsLoaded').returns(Promise.resolve());
element = fixture('basic');
- sandbox.stub(element, '_initDecoration');
- sandbox.stub(element, '_initReplacement');
+ sandbox.stub(element, '_initDecoration').returns({});
+ sandbox.stub(element, '_initReplacement').returns({});
sandbox.stub(element, 'importHref', (url, resolve) => resolve());
flush(done);
@@ -65,13 +76,39 @@
});
test('inits decoration dom hook', () => {
- assert.isTrue(
- element._initDecoration.calledWith('some-module', plugin));
+ assert.strictEqual(
+ element._initDecoration.lastCall.args[0], 'some-module');
+ assert.strictEqual(
+ element._initDecoration.lastCall.args[1], plugin);
});
test('inits replacement dom hook', () => {
- assert.isTrue(
- element._initReplacement.calledWith('other-module', plugin));
+ assert.strictEqual(
+ element._initReplacement.lastCall.args[0], 'other-module');
+ assert.strictEqual(
+ element._initReplacement.lastCall.args[1], plugin);
+ });
+
+ test('calls dom hook handleInstanceAttached', () => {
+ assert.equal(domHookStub.handleInstanceAttached.callCount, 2);
+ });
+
+ test('calls dom hook handleInstanceDetached', () => {
+ element.detached();
+ assert.equal(domHookStub.handleInstanceDetached.callCount, 2);
+ });
+
+ test('installs modules on late registration', done => {
+ domHookStub.handleInstanceAttached.reset();
+ plugin.registerCustomComponent('foo', 'noob-noob');
+ flush(() => {
+ assert.equal(domHookStub.handleInstanceAttached.callCount, 1);
+ assert.strictEqual(
+ element._initDecoration.lastCall.args[0], 'noob-noob');
+ assert.strictEqual(
+ element._initDecoration.lastCall.args[1], plugin);
+ done();
+ });
});
});
</script>
diff --git a/polygerrit-ui/app/elements/plugins/gr-popup-interface/gr-plugin-popup.html b/polygerrit-ui/app/elements/plugins/gr-popup-interface/gr-plugin-popup.html
new file mode 100644
index 0000000..3ccb3fd
--- /dev/null
+++ b/polygerrit-ui/app/elements/plugins/gr-popup-interface/gr-plugin-popup.html
@@ -0,0 +1,28 @@
+<!--
+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-overlay/gr-overlay.html">
+
+<dom-module id="gr-plugin-popup">
+ <template>
+ <style include="shared-styles"></style>
+ <gr-overlay id="overlay" with-backdrop>
+ <content></content>
+ </gr-overlay>
+ </template>
+ <script src="gr-plugin-popup.js"></script>
+</dom-module>
diff --git a/polygerrit-ui/app/elements/plugins/gr-popup-interface/gr-plugin-popup.js b/polygerrit-ui/app/elements/plugins/gr-popup-interface/gr-plugin-popup.js
new file mode 100644
index 0000000..8286eae
--- /dev/null
+++ b/polygerrit-ui/app/elements/plugins/gr-popup-interface/gr-plugin-popup.js
@@ -0,0 +1,28 @@
+// 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(window) {
+ 'use strict';
+ Polymer({
+ is: 'gr-plugin-popup',
+ get opened() {
+ return this.$.overlay.opened;
+ },
+ open() {
+ return this.$.overlay.open();
+ },
+ close() {
+ this.$.overlay.close();
+ },
+ });
+})(window);
diff --git a/polygerrit-ui/app/elements/plugins/gr-popup-interface/gr-plugin-popup_test.html b/polygerrit-ui/app/elements/plugins/gr-popup-interface/gr-plugin-popup_test.html
new file mode 100644
index 0000000..2dbf96d
--- /dev/null
+++ b/polygerrit-ui/app/elements/plugins/gr-popup-interface/gr-plugin-popup_test.html
@@ -0,0 +1,67 @@
+<!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-plugin-popup</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-plugin-popup.html"/>
+
+<script>void(0);</script>
+
+<test-fixture id="basic">
+ <template>
+ <gr-plugin-popup></gr-plugin-popup>
+ </template>
+</test-fixture>
+
+<script>
+ suite('gr-plugin-popup tests', () => {
+ let element;
+ let sandbox;
+
+ setup(() => {
+ sandbox = sinon.sandbox.create();
+ element = fixture('basic');
+ stub('gr-overlay', {
+ open: sandbox.stub().returns(Promise.resolve()),
+ close: sandbox.stub(),
+ });
+ });
+
+ teardown(() => {
+ sandbox.restore();
+ });
+
+ test('exists', () => {
+ assert.isOk(element);
+ });
+
+ test('open uses open() from gr-overlay', () => {
+ return element.open().then(() => {
+ assert.isTrue(element.$.overlay.open.called);
+ });
+ });
+
+ test('close uses close() from gr-overlay', () => {
+ element.close();
+ assert.isTrue(element.$.overlay.close.called);
+ });
+ });
+</script>
diff --git a/polygerrit-ui/app/elements/plugins/gr-popup-interface/gr-popup-interface.html b/polygerrit-ui/app/elements/plugins/gr-popup-interface/gr-popup-interface.html
new file mode 100644
index 0000000..6bf37de
--- /dev/null
+++ b/polygerrit-ui/app/elements/plugins/gr-popup-interface/gr-popup-interface.html
@@ -0,0 +1,23 @@
+<!--
+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-js-api-interface/gr-js-api-interface.html">
+<link rel="import" href="gr-plugin-popup.html">
+
+<dom-module id="gr-popup-interface">
+ <script src="gr-popup-interface.js"></script>
+</dom-module>
diff --git a/polygerrit-ui/app/elements/plugins/gr-popup-interface/gr-popup-interface.js b/polygerrit-ui/app/elements/plugins/gr-popup-interface/gr-popup-interface.js
new file mode 100644
index 0000000..e62e882
--- /dev/null
+++ b/polygerrit-ui/app/elements/plugins/gr-popup-interface/gr-popup-interface.js
@@ -0,0 +1,71 @@
+// 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(window) {
+ 'use strict';
+
+ /**
+ * Plugin popup API.
+ * Provides method for opening and closing popups from plugin.
+ * opt_moduleName is a name of custom element that will be automatically
+ * inserted on popup opening.
+ * @param {!Object} plugin
+ * @param {opt_moduleName=} string
+ */
+ function GrPopupInterface(plugin, opt_moduleName) {
+ this.plugin = plugin;
+ this._openingPromise = null;
+ this._popup = null;
+ this._moduleName = opt_moduleName || null;
+ }
+
+ GrPopupInterface.prototype._getElement = function() {
+ return Polymer.dom(this._popup);
+ };
+
+ /**
+ * Opens the popup, inserts it into DOM over current UI.
+ * Creates the popup if not previously created. Creates popup content element,
+ * if it was provided with constructor.
+ * @returns {!Promise<!Object>}
+ */
+ GrPopupInterface.prototype.open = function() {
+ if (!this._openingPromise) {
+ this._openingPromise =
+ this.plugin.hook('plugin-overlay').getLastAttached()
+ .then(hookEl => {
+ const popup = document.createElement('gr-plugin-popup');
+ if (this._moduleName) {
+ const el = Polymer.dom(popup).appendChild(
+ document.createElement(this._moduleName));
+ el.plugin = this.plugin;
+ }
+ this._popup = Polymer.dom(hookEl).appendChild(popup);
+ Polymer.dom.flush();
+ return this._popup.open().then(() => this);
+ });
+ }
+ return this._openingPromise;
+ };
+
+ /**
+ * Hides the popup.
+ */
+ GrPopupInterface.prototype.close = function() {
+ if (!this._popup) { return; }
+ this._popup.close();
+ this._openingPromise = null;
+ };
+
+ window.GrPopupInterface = GrPopupInterface;
+})(window);
diff --git a/polygerrit-ui/app/elements/plugins/gr-popup-interface/gr-popup-interface_test.html b/polygerrit-ui/app/elements/plugins/gr-popup-interface/gr-popup-interface_test.html
new file mode 100644
index 0000000..7d9dd28
--- /dev/null
+++ b/polygerrit-ui/app/elements/plugins/gr-popup-interface/gr-popup-interface_test.html
@@ -0,0 +1,112 @@
+<!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-popup-interface</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-popup-interface.html"/>
+
+<script>void(0);</script>
+
+<test-fixture id="container">
+ <template>
+ <div></div>
+ </template>
+</test-fixture>
+
+<dom-module id="gr-user-test-popup">
+ <template>
+ <div id="barfoo">some test module</div>
+ </template>
+ <script>Polymer({is: 'gr-user-test-popup'});</script>
+</dom-module>
+
+<script>
+ suite('gr-popup-interface tests', () => {
+ let container;
+ let instance;
+ let plugin;
+ let sandbox;
+
+ setup(() => {
+ sandbox = sinon.sandbox.create();
+ Gerrit.install(p => { plugin = p; }, '0.1',
+ 'http://test.com/plugins/testplugin/static/test.js');
+ container = fixture('container');
+ sandbox.stub(plugin, 'hook').returns({
+ getLastAttached() {
+ return Promise.resolve(container);
+ },
+ });
+ });
+
+ teardown(() => {
+ sandbox.restore();
+ });
+
+ suite('manual', () => {
+ setup(() => {
+ instance = new GrPopupInterface(plugin);
+ });
+
+ test('open', () => {
+ return instance.open().then(api => {
+ assert.strictEqual(api, instance);
+ const manual = document.createElement('div');
+ manual.id = 'foobar';
+ manual.innerHTML = 'manual content';
+ api._getElement().appendChild(manual);
+ flushAsynchronousOperations();
+ assert.equal(
+ container.querySelector('#foobar').textContent, 'manual content');
+ });
+ });
+
+ test('close', () => {
+ return instance.open().then(api => {
+ assert.isTrue(api._getElement().node.opened);
+ api.close();
+ assert.isFalse(api._getElement().node.opened);
+ });
+ });
+ });
+
+ suite('components', () => {
+ setup(() => {
+ instance = new GrPopupInterface(plugin, 'gr-user-test-popup');
+ });
+
+ test('open', () => {
+ return instance.open().then(api => {
+ assert.isNotNull(
+ Polymer.dom(container).querySelector('gr-user-test-popup'));
+ });
+ });
+
+ test('close', () => {
+ return instance.open().then(api => {
+ assert.isTrue(api._getElement().node.opened);
+ api.close();
+ assert.isFalse(api._getElement().node.opened);
+ });
+ });
+ });
+ });
+</script>
diff --git a/polygerrit-ui/app/elements/plugins/gr-theme-api/gr-theme-api.js b/polygerrit-ui/app/elements/plugins/gr-theme-api/gr-theme-api.js
index e91ab0a..d57b301 100644
--- a/polygerrit-ui/app/elements/plugins/gr-theme-api/gr-theme-api.js
+++ b/polygerrit-ui/app/elements/plugins/gr-theme-api/gr-theme-api.js
@@ -22,7 +22,7 @@
}
GrThemeApi.prototype.setHeaderLogoAndTitle = function(logoUrl, title) {
- this.plugin.getDomHook('header-title', {replace: true}).onAttached(
+ this.plugin.hook('header-title', {replace: true}).onAttached(
element => {
const customHeader =
document.createElement('gr-custom-plugin-header');
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 a66ab2f..ec8f04c 100644
--- a/polygerrit-ui/app/elements/shared/gr-dropdown/gr-dropdown.js
+++ b/polygerrit-ui/app/elements/shared/gr-dropdown/gr-dropdown.js
@@ -14,6 +14,9 @@
(function() {
'use strict';
+ const REL_NOOPENER = 'noopener';
+ const REL_EXTERNAL = 'external';
+
Polymer({
is: 'gr-dropdown',
@@ -83,6 +86,10 @@
'up': '_handleUp',
},
+ /**
+ * Handle the up key.
+ * @param {!Event} e
+ */
_handleUp(e) {
if (this.$.dropdown.opened) {
e.preventDefault();
@@ -93,6 +100,10 @@
}
},
+ /**
+ * Handle the down key.
+ * @param {!Event} e
+ */
_handleDown(e) {
if (this.$.dropdown.opened) {
e.preventDefault();
@@ -103,6 +114,10 @@
}
},
+ /**
+ * Handle the tab key.
+ * @param {!Event} e
+ */
_handleTab(e) {
if (this.$.dropdown.opened) {
// Tab in a native select is a no-op. Emulate this.
@@ -111,6 +126,10 @@
}
},
+ /**
+ * Handle the enter key.
+ * @param {!Event} e
+ */
_handleEnter(e) {
e.preventDefault();
e.stopPropagation();
@@ -125,6 +144,10 @@
}
},
+ /**
+ * Handle a click on the iron-dropdown element.
+ * @param {!Event} e
+ */
_handleDropdownTap(e) {
// async is needed so that that the click event is fired before the
// dropdown closes (This was a bug for touch devices).
@@ -133,10 +156,17 @@
}, 1);
},
+ /**
+ * Hanlde a click on the button to open the dropdown.
+ * @param {!Event} e
+ */
_showDropdownTapHandler(e) {
this._open();
},
+ /**
+ * Open the dropdown and initialize the cursor.
+ */
_open() {
this.$.dropdown.open();
this.$.cursor.setCursorAtIndex(0);
@@ -144,19 +174,43 @@
this.$.cursor.target.focus();
},
+ /**
+ * Get the class for a top-content item based on the given boolean.
+ * @param {boolean} bold Whether the item is bold.
+ * @return {string} The class for the top-content item.
+ */
_getClassIfBold(bold) {
return bold ? 'bold-text' : '';
},
+ /**
+ * Build a URL for the given host and path. If there is a base URL, it will
+ * be included between the host and the path.
+ * @param {!string} host
+ * @param {!string} path
+ * @return {!string} The scheme-relative URL.
+ */
_computeURLHelper(host, path) {
return '//' + host + this.getBaseUrl() + path;
},
+ /**
+ * Build a scheme-relative URL for the current host. Will include the base
+ * URL if one is present. Note: the URL will be scheme-relative but absolute
+ * with regard to the host.
+ * @param {!string} path The path for the URL.
+ * @return {!string} The scheme-relative URL.
+ */
_computeRelativeURL(path) {
const host = window.location.host;
return this._computeURLHelper(host, path);
},
+ /**
+ * Compute the URL for a link object.
+ * @param {!Object} link The object describing the link.
+ * @return {!string} The URL.
+ */
_computeLinkURL(link) {
if (typeof link.url === 'undefined') {
return '';
@@ -167,10 +221,24 @@
return this._computeRelativeURL(link.url);
},
+ /**
+ * Compute the value for the rel attribute of an anchor for the given link
+ * object. If the link has a target value, then the rel must be "noopener"
+ * for security reasons.
+ * @param {!Object} link The object describing the link.
+ * @return {?string} The rel value for the link.
+ */
_computeLinkRel(link) {
- return link.target ? 'noopener' : null;
+ // Note: noopener takes precedence over external.
+ if (link.target) { return REL_NOOPENER; }
+ if (link.external) { return REL_EXTERNAL; }
+ return null;
},
+ /**
+ * Handle a click on an item of the dropdown.
+ * @param {!Event} e
+ */
_handleItemTap(e) {
const id = e.target.getAttribute('data-id');
const item = this.items.find(item => item.id === id);
@@ -182,10 +250,20 @@
}
},
+ /**
+ * If a dropdown item is shown as a button, get the class for the button.
+ * @param {string} id
+ * @param {!Object} disabledIdsRecord The change record for the disabled IDs
+ * list.
+ * @return {!string} The class for the item button.
+ */
_computeDisabledClass(id, disabledIdsRecord) {
return disabledIdsRecord.base.includes(id) ? 'disabled' : '';
},
+ /**
+ * Recompute the stops for the dropdown item cursor.
+ */
_resetCursorStops() {
Polymer.dom.flush();
this._listElements = Polymer.dom(this.root).querySelectorAll('li');
diff --git a/polygerrit-ui/app/elements/shared/gr-dropdown/gr-dropdown_test.html b/polygerrit-ui/app/elements/shared/gr-dropdown/gr-dropdown_test.html
index 8654ac8..ab31f7c 100644
--- a/polygerrit-ui/app/elements/shared/gr-dropdown/gr-dropdown_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-dropdown/gr-dropdown_test.html
@@ -72,10 +72,17 @@
});
test('link rel', () => {
- assert.isNull(element._computeLinkRel({url: '/test'}));
- assert.equal(
- element._computeLinkRel({url: '/test', target: '_blank'}),
- 'noopener');
+ let link = {url: '/test'};
+ assert.isNull(element._computeLinkRel(link));
+
+ link = {url: '/test', target: '_blank'};
+ assert.equal(element._computeLinkRel(link), 'noopener');
+
+ link = {url: '/test', external: true};
+ assert.equal(element._computeLinkRel(link), 'external');
+
+ link = {url: '/test', target: '_blank', external: true};
+ assert.equal(element._computeLinkRel(link), 'noopener');
});
test('_getClassIfBold', () => {
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-change-reply-js-api.js b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-change-reply-js-api.js
index df407a9..65aa364 100644
--- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-change-reply-js-api.js
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-change-reply-js-api.js
@@ -49,7 +49,7 @@
GrChangeReplyInterface.prototype.addReplyTextChangedCallback =
function(handler) {
- this.plugin.getDomHook('reply-text').onAttached(el => {
+ this.plugin.hook('reply-text').onAttached(el => {
if (!el.content) { return; }
el.content.addEventListener('value-changed', e => {
handler(e.detail.value);
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface.html b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface.html
index f6e2b64..53f889f 100644
--- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface.html
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface.html
@@ -19,6 +19,7 @@
<link rel="import" href="../../core/gr-reporting/gr-reporting.html">
<link rel="import" href="../../plugins/gr-attribute-helper/gr-attribute-helper.html">
<link rel="import" href="../../plugins/gr-dom-hooks/gr-dom-hooks.html">
+<link rel="import" href="../../plugins/gr-popup-interface/gr-popup-interface.html">
<link rel="import" href="../../plugins/gr-theme-api/gr-theme-api.html">
<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
<link rel="import" href="../gr-rest-api-interface/gr-rest-api-interface.html">
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface_test.html b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface_test.html
index ca0f372..dec2dc3d 100644
--- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface_test.html
@@ -120,6 +120,26 @@
});
});
+ test('delete works', () => {
+ const response = {status: 204};
+ sendStub.returns(Promise.resolve(response));
+ return plugin.delete('/url', r => {
+ assert.isTrue(sendStub.calledWith('DELETE', '/url'));
+ assert.strictEqual(r, response);
+ });
+ });
+
+ test('delete fails', () => {
+ sendStub.returns(Promise.resolve(
+ {status: 400, text() { return Promise.resolve('text'); }}));
+ return plugin.delete('/url', r => {
+ throw new Error('Should not resolve');
+ }).catch(err => {
+ assert.isTrue(sendStub.calledWith('DELETE', '/url'));
+ assert.equal('text', err);
+ });
+ });
+
test('history event', done => {
plugin.on(element.EventType.HISTORY, throwErrFn);
plugin.on(element.EventType.HISTORY, path => {
@@ -338,5 +358,35 @@
'http://test.com/r/plugins/testplugin/static/test.js');
});
});
+
+ suite('popup', () => {
+ test('popup(element) is deprecated', () => {
+ assert.throws(() => {
+ plugin.popup(document.createElement('div'));
+ });
+ });
+
+ test('popup(moduleName) creates popup with component', () => {
+ const openStub = sandbox.stub();
+ sandbox.stub(window, 'GrPopupInterface').returns({
+ open: openStub,
+ });
+ plugin.popup('some-name');
+ assert.isTrue(openStub.calledOnce);
+ assert.isTrue(GrPopupInterface.calledWith(plugin, 'some-name'));
+ });
+
+ test('deprecated.popup(element) creates popup with element', () => {
+ const el = document.createElement('div');
+ el.textContent = 'some text here';
+ const openStub = sandbox.stub(GrPopupInterface.prototype, 'open');
+ openStub.returns(Promise.resolve({
+ _getElement() {
+ return document.createElement('div');
+ }}));
+ plugin.deprecated.popup(el);
+ assert.isTrue(openStub.calledOnce);
+ });
+ });
});
</script>
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-endpoints.js b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-endpoints.js
index 52b1fb7..1ee9eec 100644
--- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-endpoints.js
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-endpoints.js
@@ -16,19 +16,32 @@
function GrPluginEndpoints() {
this._endpoints = {};
+ this._callbacks = {};
}
+ GrPluginEndpoints.prototype.onNewEndpoint = function(endpoint, callback) {
+ if (!this._callbacks[endpoint]) {
+ this._callbacks[endpoint] = [];
+ }
+ this._callbacks[endpoint].push(callback);
+ };
+
GrPluginEndpoints.prototype.registerModule = function(plugin, endpoint, type,
- moduleName) {
+ moduleName, domHook) {
if (!this._endpoints[endpoint]) {
this._endpoints[endpoint] = [];
}
- this._endpoints[endpoint].push({
+ const moduleInfo = {
moduleName,
plugin,
pluginUrl: plugin._url,
type,
- });
+ domHook,
+ };
+ this._endpoints[endpoint].push(moduleInfo);
+ if (Gerrit._arePluginsLoaded() && this._callbacks[endpoint]) {
+ this._callbacks[endpoint].forEach(callback => callback(moduleInfo));
+ }
};
/**
@@ -44,6 +57,7 @@
* plugin: Plugin,
* pluginUrl: String,
* type: EndpointType,
+ * domHook: !Object
* }>}
*/
GrPluginEndpoints.prototype.getDetails = function(name, opt_options) {
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-endpoints_test.html b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-endpoints_test.html
index 2c1f4e9..a61cdc8 100644
--- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-endpoints_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-endpoints_test.html
@@ -29,16 +29,21 @@
let instance;
let pluginFoo;
let pluginBar;
+ let domHook;
setup(() => {
sandbox = sinon.sandbox.create();
+ domHook = {};
instance = new GrPluginEndpoints();
Gerrit.install(p => { pluginFoo = p; }, '0.1',
'http://test.com/plugins/testplugin/static/foo.html');
- instance.registerModule(pluginFoo, 'a-place', 'decorate', 'foo-module');
+ instance.registerModule(
+ pluginFoo, 'a-place', 'decorate', 'foo-module', domHook);
Gerrit.install(p => { pluginBar = p; }, '0.1',
'http://test.com/plugins/testplugin/static/bar.html');
- instance.registerModule(pluginBar, 'a-place', 'style', 'bar-module');
+ instance.registerModule(
+ pluginBar, 'a-place', 'style', 'bar-module', domHook);
+ sandbox.stub(Gerrit, '_arePluginsLoaded').returns(true);
});
teardown(() => {
@@ -52,12 +57,14 @@
plugin: pluginFoo,
pluginUrl: pluginFoo._url,
type: 'decorate',
+ domHook,
},
{
moduleName: 'bar-module',
plugin: pluginBar,
pluginUrl: pluginBar._url,
type: 'style',
+ domHook,
},
]);
});
@@ -69,6 +76,7 @@
plugin: pluginBar,
pluginUrl: pluginBar._url,
type: 'style',
+ domHook,
},
]);
});
@@ -82,6 +90,7 @@
plugin: pluginFoo,
pluginUrl: pluginFoo._url,
type: 'decorate',
+ domHook,
},
]);
});
@@ -95,5 +104,19 @@
assert.deepEqual(
instance.getPlugins('a-place'), [pluginFoo._url, pluginBar._url]);
});
+
+ test('onNewEndpoint', () => {
+ const newModuleStub = sandbox.stub();
+ instance.onNewEndpoint('a-place', newModuleStub);
+ instance.registerModule(
+ pluginFoo, 'a-place', 'replace', 'zaz-module', domHook);
+ assert.deepEqual(newModuleStub.lastCall.args[0], {
+ moduleName: 'zaz-module',
+ plugin: pluginFoo,
+ pluginUrl: pluginFoo._url,
+ type: 'replace',
+ domHook,
+ });
+ });
});
</script>
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-public-js-api.js b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-public-js-api.js
index d1b9417..52db873 100644
--- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-public-js-api.js
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-public-js-api.js
@@ -55,8 +55,7 @@
window.$wnd = window;
function Plugin(opt_url) {
- this._generatedHookNames = [];
- this._domHooks = new GrDomHooks(this);
+ this._domHooks = new GrDomHooksManager(this);
if (!opt_url) {
console.warn('Plugin not being loaded from /plugins base path.',
@@ -77,6 +76,10 @@
return;
}
this._name = pathname.split('/')[2];
+
+ this.deprecated = {
+ popup: deprecatedAPI.popup.bind(this),
+ };
}
Plugin._sharedAPIElement = document.createElement('gr-js-api-interface');
@@ -93,11 +96,22 @@
};
Plugin.prototype.registerCustomComponent = function(
- endpointName, moduleName, opt_options) {
+ endpointName, opt_moduleName, opt_options) {
const type = opt_options && opt_options.replace ?
EndpointType.REPLACE : EndpointType.DECORATE;
+ const hook = this._domHooks.getDomHook(endpointName, opt_moduleName);
+ const moduleName = opt_moduleName || hook.getModuleName();
Gerrit._endpoints.registerModule(
- this, endpointName, type, moduleName);
+ this, endpointName, type, moduleName, hook);
+ return hook.getPublicAPI();
+ };
+
+ /**
+ * Returns instance of DOM hook API for endpoint. Creates a placeholder
+ * element for the first call.
+ */
+ Plugin.prototype.hook = function(endpointName, opt_options) {
+ return this.registerCustomComponent(endpointName, undefined, opt_options);
};
Plugin.prototype.getServerInfo = function() {
@@ -143,6 +157,24 @@
return this._send('POST', url, opt_callback, payload);
},
+ Plugin.prototype.delete = function(url, opt_callback) {
+ return getRestAPI().send('DELETE', url, opt_callback).then(response => {
+ if (response.status !== 204) {
+ return response.text().then(text => {
+ if (text) {
+ return Promise.reject(text);
+ } else {
+ return Promise.reject(response.status);
+ }
+ });
+ }
+ if (opt_callback) {
+ opt_callback(response);
+ }
+ return response;
+ });
+ },
+
Plugin.prototype.changeActions = function() {
return new GrChangeActionsInterface(Plugin._sharedAPIElement.getElement(
Plugin._sharedAPIElement.Element.CHANGE_ACTIONS));
@@ -166,12 +198,22 @@
return new GrAttributeHelper(element);
};
- Plugin.prototype.getDomHook = function(endpointName, opt_options) {
- const hook = this._domHooks.getDomHook(endpointName);
- const moduleName = hook.getModuleName();
- const type = opt_options && opt_options.type || EndpointType.DECORATE;
- Gerrit._endpoints.registerModule(this, endpointName, type, moduleName);
- return hook;
+ Plugin.prototype.popup = function(moduleName) {
+ if (typeof moduleName !== 'string') {
+ throw new Error('deprecated, use deprecated.popup');
+ }
+ const api = new GrPopupInterface(this, moduleName);
+ return api.open();
+ };
+
+ const deprecatedAPI = {};
+ deprecatedAPI.popup = function(el) {
+ console.warn('plugin.deprecated.popup() is deprecated!');
+ if (!el) {
+ throw new Error('Popup contents not found');
+ }
+ const api = new GrPopupInterface(this);
+ api.open().then(api => api._getElement().appendChild(el));
};
const Gerrit = window.Gerrit || {};
diff --git a/polygerrit-ui/app/index.html b/polygerrit-ui/app/index.html
index 46fc46b..080c345 100644
--- a/polygerrit-ui/app/index.html
+++ b/polygerrit-ui/app/index.html
@@ -26,6 +26,10 @@
-->
<link rel="preload" href="/fonts/RobotoMono-Regular.woff2" as="font" type="font/woff2" crossorigin>
<link rel="preload" href="/fonts/RobotoMono-Regular.woff" as="font" type="font/woff" crossorigin>
+<link rel="preload" href="/fonts/Roboto-Regular.woff2" as="font" type="font/woff2" crossorigin>
+<link rel="preload" href="/fonts/Roboto-Regular.woff" as="font" type="font/woff" crossorigin>
+<link rel="preload" href="/fonts/Roboto-Medium.woff2" as="font" type="font/woff2" crossorigin>
+<link rel="preload" href="/fonts/Roboto-Medium.woff" as="font" type="font/woff" crossorigin>
<link rel="stylesheet" href="/styles/fonts.css">
<link rel="stylesheet" href="/styles/main.css">
<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
diff --git a/polygerrit-ui/app/rules.bzl b/polygerrit-ui/app/rules.bzl
index be80c13..a77ad50 100644
--- a/polygerrit-ui/app/rules.bzl
+++ b/polygerrit-ui/app/rules.bzl
@@ -72,7 +72,7 @@
name + "_app_sources",
name + "_css_sources",
name + "_top_sources",
- "//lib/fonts:robotomono",
+ "//lib/fonts:robotofonts",
"//lib/js:highlightjs_files",
# we extract from the zip, but depend on the component for license checking.
"@webcomponentsjs//:zipfile",
@@ -82,7 +82,7 @@
cmd = " && ".join([
"mkdir -p $$TMP/polygerrit_ui/{styles,fonts,bower_components/{highlightjs,webcomponentsjs},elements}",
"for f in $(locations " + name + "_app_sources); do ext=$${f##*.}; cp -p $$f $$TMP/polygerrit_ui/elements/" + appName + ".$$ext; done",
- "cp $(locations //lib/fonts:robotomono) $$TMP/polygerrit_ui/fonts/",
+ "cp $(locations //lib/fonts:robotofonts) $$TMP/polygerrit_ui/fonts/",
"for f in $(locations " + name + "_top_sources); do cp $$f $$TMP/polygerrit_ui/; done",
"for f in $(locations "+ name + "_css_sources); do cp $$f $$TMP/polygerrit_ui/styles; done",
"for f in $(locations //lib/js:highlightjs_files); do cp $$f $$TMP/polygerrit_ui/bower_components/highlightjs/ ; done",
diff --git a/polygerrit-ui/app/styles/app-theme.html b/polygerrit-ui/app/styles/app-theme.html
index 4728fe6..db83b0c 100644
--- a/polygerrit-ui/app/styles/app-theme.html
+++ b/polygerrit-ui/app/styles/app-theme.html
@@ -29,11 +29,11 @@
--default-text-color: #000;
--view-background-color: #fff;
--default-horizontal-margin: 1rem;
- --font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol';
+ --font-family: 'Roboto', -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol';
--monospace-font-family: 'Roboto Mono', Menlo, 'Lucida Console', Monaco, monospace;
--iron-overlay-backdrop: {
transition: none;
- };
+ }
}
@media screen and (max-width: 50em) {
:root {
diff --git a/polygerrit-ui/app/styles/fonts.css b/polygerrit-ui/app/styles/fonts.css
index d339e16..6a5da44 100644
--- a/polygerrit-ui/app/styles/fonts.css
+++ b/polygerrit-ui/app/styles/fonts.css
@@ -17,4 +17,46 @@
url('../fonts/RobotoMono-Regular.woff2') format('woff2'),
url('../fonts/RobotoMono-Regular.woff') format('woff');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2212, U+2215, U+E0FF, U+EFFD, U+F000;
+}
+
+/* latin-ext */
+@font-face {
+ font-family: 'Roboto';
+ font-style: normal;
+ font-weight: 400;
+ src: local('Roboto'), local('Roboto-Regular'),
+ url('../fonts/Roboto-Regular.woff2') format('woff2'),
+ url('../fonts/Roboto-Regular.woff') format('woff');
+ unicode-range: U+0100-024F, U+1E00-1EFF, U+20A0-20AB, U+20AD-20CF, U+2C60-2C7F, U+A720-A7FF;
+}
+/* latin */
+@font-face {
+ font-family: 'Roboto';
+ font-style: normal;
+ font-weight: 400;
+ src: local('Roboto'), local('RobotoMono-Regular'),
+ url('../fonts/Roboto-Regular.woff2') format('woff2'),
+ url('../fonts/Roboto-Regular.woff') format('woff');
+ unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2212, U+2215, U+E0FF, U+EFFD, U+F000;
+}
+
+/* latin-ext */
+@font-face {
+ font-family: 'Roboto Medium';
+ font-style: normal;
+ font-weight: 400;
+ src: local('Roboto Medium'), local('Roboto-Medium'),
+ url('../fonts/Roboto-Medium.woff2') format('woff2'),
+ url('../fonts/Roboto-Medium.woff') format('woff');
+ unicode-range: U+0100-024F, U+1E00-1EFF, U+20A0-20AB, U+20AD-20CF, U+2C60-2C7F, U+A720-A7FF;
+}
+/* latin */
+@font-face {
+ font-family: 'Roboto Medium';
+ font-style: normal;
+ font-weight: 400;
+ src: local('Roboto Medium'), local('Roboto-Medium'),
+ url('../fonts/Roboto-Medium.woff2') format('woff2'),
+ url('../fonts/Roboto-Medium.woff') format('woff');
+ unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2212, U+2215, U+E0FF, U+EFFD, U+F000;
}
\ No newline at end of file
diff --git a/polygerrit-ui/app/styles/main.css b/polygerrit-ui/app/styles/main.css
index b18543a..045821c 100644
--- a/polygerrit-ui/app/styles/main.css
+++ b/polygerrit-ui/app/styles/main.css
@@ -37,6 +37,6 @@
*/
-webkit-text-size-adjust: none;
font-size: 13px;
- font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol';
+ font-family: 'Roboto', -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol';
line-height: 1.4;
}
diff --git a/polygerrit-ui/app/styles/shared-styles.html b/polygerrit-ui/app/styles/shared-styles.html
index cc1dabd..9725251 100644
--- a/polygerrit-ui/app/styles/shared-styles.html
+++ b/polygerrit-ui/app/styles/shared-styles.html
@@ -38,6 +38,12 @@
margin: 0;
padding: 0;
}
+ input,
+ textarea,
+ select,
+ button {
+ font: inherit;
+ }
body {
line-height: 1;
}
diff --git a/polygerrit-ui/app/template_test_srcs/template_test.js b/polygerrit-ui/app/template_test_srcs/template_test.js
index 138d0ea..dffcaf9 100644
--- a/polygerrit-ui/app/template_test_srcs/template_test.js
+++ b/polygerrit-ui/app/template_test_srcs/template_test.js
@@ -27,13 +27,15 @@
'GrGerritAuth',
'GrLinkTextParser',
'GrPluginEndpoints',
+ 'GrPopupInterface',
'GrRangeNormalizer',
'GrReporting',
'GrReviewerUpdatesParser',
'GrThemeApi',
'moment',
'page',
- 'util'];
+ 'util',
+];
fs.readdir('./polygerrit-ui/temp/behaviors/', (err, data) => {
if (err) {
@@ -102,4 +104,4 @@
process.exit(1);
}
});
-});
\ No newline at end of file
+});
diff --git a/polygerrit-ui/app/test/index.html b/polygerrit-ui/app/test/index.html
index 41f5fc6..912b0ff 100644
--- a/polygerrit-ui/app/test/index.html
+++ b/polygerrit-ui/app/test/index.html
@@ -103,6 +103,8 @@
'plugins/gr-attribute-helper/gr-attribute-helper_test.html',
'plugins/gr-external-style/gr-external-style_test.html',
'plugins/gr-plugin-host/gr-plugin-host_test.html',
+ 'plugins/gr-popup-interface/gr-plugin-popup_test.html',
+ 'plugins/gr-popup-interface/gr-popup-interface_test.html',
'settings/gr-account-info/gr-account-info_test.html',
'settings/gr-change-table-editor/gr-change-table-editor_test.html',
'settings/gr-email-editor/gr-email-editor_test.html',