Merge "Submit requirements: always hide applicability expressions on the API"
diff --git a/Documentation/config-gerrit.txt b/Documentation/config-gerrit.txt
index acf65a5..3109ec7 100644
--- a/Documentation/config-gerrit.txt
+++ b/Documentation/config-gerrit.txt
@@ -1169,6 +1169,14 @@
Result of checking if one change or commit is a pure/clean revert of
another.
+cache `"soy_sauce_compiled_templates"`::
++
+Caches compiled soy templates. Stores at most only one key-value pair with
+a constant key value and the value is a compiled SoySauce templates. The value
+is reloaded automatically every few seconds if there are reads from the cache.
+If cache is not used for 1 minute, the item is removed (i.e. emails can be send
+with templates which are max 1 minute old).
+
cache `"sshkeys"`::
+
Caches unpacked versions of user SSH keys, so the internal SSH daemon
diff --git a/Documentation/metrics.txt b/Documentation/metrics.txt
index 0318cd7..d8b6250 100644
--- a/Documentation/metrics.txt
+++ b/Documentation/metrics.txt
@@ -113,7 +113,7 @@
* `receivecommits/ps_revision_missing`: errors due to patch set revision missing
* `receivecommits/push_count`: number of pushes
** `kind`:
- The push kind (direct vs. magic).
+ The push kind (magic, direct or direct_submit).
** `project`:
The name of the project for which the push is done.
** `type`:
diff --git a/Documentation/pgm-daemon.txt b/Documentation/pgm-daemon.txt
index cf6560b..586f685 100644
--- a/Documentation/pgm-daemon.txt
+++ b/Documentation/pgm-daemon.txt
@@ -61,7 +61,7 @@
Run init before starting the daemon. This will create a new site or
upgrade an existing site.
---s::
+-s::
Start link:dev-inspector.html[Gerrit Inspector] on the console, a
built-in interactive inspection environment to assist debugging and
troubleshooting of Gerrit code.
diff --git a/Documentation/user-notify.txt b/Documentation/user-notify.txt
index 128bae6..20ad07c 100644
--- a/Documentation/user-notify.txt
+++ b/Documentation/user-notify.txt
@@ -11,9 +11,9 @@
Those are the available recipient types:
-* `to`: The standard To field is used; addresses are visible to all.
-* `cc`: The standard CC field is used; addresses are visible to all.
-* `bcc`: SMTP RCPT TO is used to hide the address.
+* `TO`: The standard To field is used; addresses are visible to all.
+* `CC`: The standard CC field is used; addresses are visible to all.
+* `BCC`: SMTP RCPT TO is used to hide the address.
[[user]]
== User Level Settings
diff --git a/Documentation/user-search.txt b/Documentation/user-search.txt
index cc8d813..6e6b9d7 100644
--- a/Documentation/user-search.txt
+++ b/Documentation/user-search.txt
@@ -565,6 +565,11 @@
cherry-picked locally using the git cherry-pick command and then
pushed to Gerrit.
+[[pure-revert]]
+is:pure-revert::
++
+True if the change is a pure revert.
+
[[status]]
status:open, status:pending, status:new::
+
@@ -772,6 +777,22 @@
+
Matches changes with label voted with any score.
+`label:Code-Review=+1,count=2`::
++
+Matches changes with exactly two +1 votes to the code-review label. The {MAX,
+MIN, ANY} votes can also be used, for example `label:Code-Review=MAX,count=2` is
+equivalent to `label:Code-Review=2,count=2` (if 2 is the maximum positive vote
+for the code review label). The maximum supported value for `count` is 5.
+`count=0` is not allowed and the query request will fail with `400 Bad Request`.
+
+`label:Code-Review=+1,count>=2`::
++
+Matches changes having two or more +1 votes to the code-review label. Can also
+be used with the {MAX, MIN, ANY} label votes. All operators `>`, `>=`, `<`, `<=`
+are supported.
+Note that a query like `label:Code-Review=+1,count<x` will not match with
+changes having zero +1 votes to this label.
+
`label:Non-Author-Code-Review=need`::
+
Matches changes where the submit rules indicate that a label named
diff --git a/java/com/google/gerrit/server/change/BatchAbandon.java b/java/com/google/gerrit/server/change/BatchAbandon.java
index e0a72ac4..9a3c388 100644
--- a/java/com/google/gerrit/server/change/BatchAbandon.java
+++ b/java/com/google/gerrit/server/change/BatchAbandon.java
@@ -20,6 +20,7 @@
import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.account.AccountState;
import com.google.gerrit.server.config.ChangeCleanupConfig;
+import com.google.gerrit.server.notedb.StoreSubmitRequirementsOp;
import com.google.gerrit.server.plugincontext.PluginItemContext;
import com.google.gerrit.server.query.change.ChangeData;
import com.google.gerrit.server.update.BatchUpdate;
@@ -34,15 +35,18 @@
private final AbandonOp.Factory abandonOpFactory;
private final ChangeCleanupConfig cfg;
private final PluginItemContext<AccountPatchReviewStore> accountPatchReviewStore;
+ private final StoreSubmitRequirementsOp.Factory storeSubmitRequirementsOpFactory;
@Inject
BatchAbandon(
AbandonOp.Factory abandonOpFactory,
ChangeCleanupConfig cfg,
- PluginItemContext<AccountPatchReviewStore> accountPatchReviewStore) {
+ PluginItemContext<AccountPatchReviewStore> accountPatchReviewStore,
+ StoreSubmitRequirementsOp.Factory storeSubmitRequirementsOpFactory) {
this.abandonOpFactory = abandonOpFactory;
this.cfg = cfg;
this.accountPatchReviewStore = accountPatchReviewStore;
+ this.storeSubmitRequirementsOpFactory = storeSubmitRequirementsOpFactory;
}
/**
@@ -74,6 +78,7 @@
change.project().get(), project.get()));
}
u.addOp(change.getId(), abandonOpFactory.create(accountState, msgTxt));
+ u.addOp(change.getId(), storeSubmitRequirementsOpFactory.create());
}
u.execute();
diff --git a/java/com/google/gerrit/server/config/GerritGlobalModule.java b/java/com/google/gerrit/server/config/GerritGlobalModule.java
index 6c25bae..24882cb 100644
--- a/java/com/google/gerrit/server/config/GerritGlobalModule.java
+++ b/java/com/google/gerrit/server/config/GerritGlobalModule.java
@@ -168,9 +168,8 @@
import com.google.gerrit.server.mail.send.FromAddressGenerator;
import com.google.gerrit.server.mail.send.FromAddressGeneratorProvider;
import com.google.gerrit.server.mail.send.InboundEmailRejectionSender;
-import com.google.gerrit.server.mail.send.MailSoySauceProvider;
+import com.google.gerrit.server.mail.send.MailSoySauceModule;
import com.google.gerrit.server.mail.send.MailSoyTemplateProvider;
-import com.google.gerrit.server.mail.send.MailTemplates;
import com.google.gerrit.server.mime.FileTypeRegistry;
import com.google.gerrit.server.mime.MimeUtilFileTypeRegistry;
import com.google.gerrit.server.notedb.NoteDbModule;
@@ -227,7 +226,6 @@
import com.google.inject.TypeLiteral;
import com.google.inject.internal.UniqueAnnotations;
import com.google.inject.multibindings.OptionalBinder;
-import com.google.template.soy.jbcsrc.api.SoySauce;
import java.util.List;
import org.eclipse.jgit.lib.Config;
import org.eclipse.jgit.transport.PostReceiveHook;
@@ -289,6 +287,7 @@
install(new FileInfoJsonModule());
install(ThreadLocalRequestContext.module());
install(new ApprovalModule());
+ install(new MailSoySauceModule());
factory(CapabilityCollection.Factory.class);
factory(ChangeData.AssistedFactory.class);
@@ -330,7 +329,6 @@
bind(ApprovalsUtil.class);
- bind(SoySauce.class).annotatedWith(MailTemplates.class).toProvider(MailSoySauceProvider.class);
bind(FromAddressGenerator.class).toProvider(FromAddressGeneratorProvider.class).in(SINGLETON);
bind(Boolean.class)
.annotatedWith(EnablePeerIPInReflogRecord.class)
diff --git a/java/com/google/gerrit/server/git/receive/ReceiveCommits.java b/java/com/google/gerrit/server/git/receive/ReceiveCommits.java
index 66e7d80..f4c7a92 100644
--- a/java/com/google/gerrit/server/git/receive/ReceiveCommits.java
+++ b/java/com/google/gerrit/server/git/receive/ReceiveCommits.java
@@ -747,14 +747,19 @@
return;
}
- if (!magicCommands.isEmpty()) {
- metrics.pushCount.increment("magic", project.getName(), getUpdateType(magicCommands));
- }
- if (!regularCommands.isEmpty()) {
- metrics.pushCount.increment("direct", project.getName(), getUpdateType(regularCommands));
- }
-
try {
+ if (!magicCommands.isEmpty()) {
+ parseMagicBranch(Iterables.getLast(magicCommands));
+ // Using the submit option submits the created change(s) immediately without checking labels
+ // nor submit rules. Hence we shouldn't record such pushes as "magic" which implies that
+ // code review is being done.
+ String pushKind = magicBranch != null && magicBranch.submit ? "direct_submit" : "magic";
+ metrics.pushCount.increment(pushKind, project.getName(), getUpdateType(magicCommands));
+ }
+ if (!regularCommands.isEmpty()) {
+ metrics.pushCount.increment("direct", project.getName(), getUpdateType(regularCommands));
+ }
+
if (!regularCommands.isEmpty()) {
handleRegularCommands(regularCommands, progress);
return;
@@ -763,7 +768,6 @@
boolean first = true;
for (ReceiveCommand cmd : magicCommands) {
if (first) {
- parseMagicBranch(cmd);
first = false;
} else {
reject(cmd, "duplicate request");
diff --git a/java/com/google/gerrit/server/git/receive/ReceiveConstants.java b/java/com/google/gerrit/server/git/receive/ReceiveConstants.java
index df1888b..e50482d 100644
--- a/java/com/google/gerrit/server/git/receive/ReceiveConstants.java
+++ b/java/com/google/gerrit/server/git/receive/ReceiveConstants.java
@@ -21,7 +21,7 @@
@VisibleForTesting
public static final String ONLY_USERS_WITH_TOGGLE_WIP_STATE_PERM_CAN_MODIFY_WIP =
- "only users with Toogle-Wip-State permission can modify Work-in-Progress";
+ "only users with Toggle-Wip-State permission can modify Work-in-Progress";
static final String COMMAND_REJECTION_MESSAGE_FOOTER =
"Contact an administrator to fix the permissions";
diff --git a/java/com/google/gerrit/server/index/change/ChangeField.java b/java/com/google/gerrit/server/index/change/ChangeField.java
index b9569e4..2cdb7c8 100644
--- a/java/com/google/gerrit/server/index/change/ChangeField.java
+++ b/java/com/google/gerrit/server/index/change/ChangeField.java
@@ -33,6 +33,7 @@
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Splitter;
+import com.google.common.collect.HashBasedTable;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
@@ -412,6 +413,10 @@
integer(ChangeQueryBuilder.FIELD_REVERTOF)
.build(cd -> cd.change().getRevertOf() != null ? cd.change().getRevertOf().get() : null);
+ public static final FieldDef<ChangeData, String> IS_PURE_REVERT =
+ fullText(ChangeQueryBuilder.FIELD_PURE_REVERT)
+ .build(cd -> Boolean.TRUE.equals(cd.isPureRevert()) ? "1" : "0");
+
@VisibleForTesting
static List<String> getReviewerFieldValues(ReviewerSet reviewers) {
List<String> r = new ArrayList<>(reviewers.asTable().size() * 2);
@@ -612,47 +617,107 @@
private static Iterable<String> getLabels(ChangeData cd) {
Set<String> allApprovals = new HashSet<>();
Set<String> distinctApprovals = new HashSet<>();
+ Table<String, Short, Integer> voteCounts = HashBasedTable.create();
for (PatchSetApproval a : cd.currentApprovals()) {
if (a.value() != 0 && !a.isLegacySubmit()) {
- allApprovals.add(formatLabel(a.label(), a.value(), a.accountId()));
+ increment(voteCounts, a.label(), a.value());
Optional<LabelType> labelType = cd.getLabelTypes().byLabel(a.labelId());
- allApprovals.addAll(getMaxMinAnyLabels(a.label(), a.value(), labelType, a.accountId()));
- if (cd.change().getOwner().equals(a.accountId())) {
- allApprovals.add(formatLabel(a.label(), a.value(), ChangeQueryBuilder.OWNER_ACCOUNT_ID));
- allApprovals.addAll(
- getMaxMinAnyLabels(
- a.label(), a.value(), labelType, ChangeQueryBuilder.OWNER_ACCOUNT_ID));
- }
- if (!cd.currentPatchSet().uploader().equals(a.accountId())) {
- allApprovals.add(
- formatLabel(a.label(), a.value(), ChangeQueryBuilder.NON_UPLOADER_ACCOUNT_ID));
- allApprovals.addAll(
- getMaxMinAnyLabels(
- a.label(), a.value(), labelType, ChangeQueryBuilder.NON_UPLOADER_ACCOUNT_ID));
- }
+
+ allApprovals.add(formatLabel(a.label(), a.value(), a.accountId()));
+ allApprovals.addAll(getMagicLabelFormats(a.label(), a.value(), labelType, a.accountId()));
+ allApprovals.addAll(getLabelOwnerFormats(a, cd, labelType));
+ allApprovals.addAll(getLabelNonUploaderFormats(a, cd, labelType));
distinctApprovals.add(formatLabel(a.label(), a.value()));
- distinctApprovals.addAll(getMaxMinAnyLabels(a.label(), a.value(), labelType, null));
+ distinctApprovals.addAll(
+ getMagicLabelFormats(a.label(), a.value(), labelType, /* accountId= */ null));
}
}
allApprovals.addAll(distinctApprovals);
+ allApprovals.addAll(getCountLabelFormats(voteCounts, cd));
return allApprovals;
}
- private static List<String> getMaxMinAnyLabels(
+ private static void increment(Table<String, Short, Integer> table, String k1, short k2) {
+ if (!table.contains(k1, k2)) {
+ table.put(k1, k2, 1);
+ } else {
+ int val = table.get(k1, k2);
+ table.put(k1, k2, val + 1);
+ }
+ }
+
+ private static List<String> getCountLabelFormats(
+ Table<String, Short, Integer> voteCounts, ChangeData cd) {
+ List<String> allFormats = new ArrayList<>();
+ for (String label : voteCounts.rowMap().keySet()) {
+ Optional<LabelType> labelType = cd.getLabelTypes().byLabel(label);
+ Map<Short, Integer> row = voteCounts.row(label);
+ for (short vote : row.keySet()) {
+ int count = row.get(vote);
+ allFormats.addAll(getCountLabelFormats(labelType, label, vote, count));
+ }
+ }
+ return allFormats;
+ }
+
+ private static List<String> getCountLabelFormats(
+ Optional<LabelType> labelType, String label, short vote, int count) {
+ List<String> formats =
+ getMagicLabelFormats(label, vote, labelType, /* accountId= */ null, /* count= */ count);
+ formats.add(formatLabel(label, vote, count));
+ return formats;
+ }
+
+ /** Get magic label formats corresponding to the {MIN, MAX, ANY} label votes. */
+ private static List<String> getMagicLabelFormats(
String label, short labelVal, Optional<LabelType> labelType, @Nullable Account.Id accountId) {
+ return getMagicLabelFormats(label, labelVal, labelType, accountId, /* count= */ null);
+ }
+
+ /** Get magic label formats corresponding to the {MIN, MAX, ANY} label votes. */
+ private static List<String> getMagicLabelFormats(
+ String label,
+ short labelVal,
+ Optional<LabelType> labelType,
+ @Nullable Account.Id accountId,
+ @Nullable Integer count) {
List<String> labels = new ArrayList<>();
if (labelType.isPresent()) {
if (labelVal == labelType.get().getMaxPositive()) {
- labels.add(formatLabel(label, MagicLabelValue.MAX.name(), accountId));
+ labels.add(formatLabel(label, MagicLabelValue.MAX.name(), accountId, count));
}
if (labelVal == labelType.get().getMaxNegative()) {
- labels.add(formatLabel(label, MagicLabelValue.MIN.name(), accountId));
+ labels.add(formatLabel(label, MagicLabelValue.MIN.name(), accountId, count));
}
}
- labels.add(formatLabel(label, MagicLabelValue.ANY.name(), accountId));
+ labels.add(formatLabel(label, MagicLabelValue.ANY.name(), accountId, count));
return labels;
}
+ private static List<String> getLabelOwnerFormats(
+ PatchSetApproval a, ChangeData cd, Optional<LabelType> labelType) {
+ List<String> allFormats = new ArrayList<>();
+ if (cd.change().getOwner().equals(a.accountId())) {
+ allFormats.add(formatLabel(a.label(), a.value(), ChangeQueryBuilder.OWNER_ACCOUNT_ID));
+ allFormats.addAll(
+ getMagicLabelFormats(
+ a.label(), a.value(), labelType, ChangeQueryBuilder.OWNER_ACCOUNT_ID));
+ }
+ return allFormats;
+ }
+
+ private static List<String> getLabelNonUploaderFormats(
+ PatchSetApproval a, ChangeData cd, Optional<LabelType> labelType) {
+ List<String> allFormats = new ArrayList<>();
+ if (!cd.currentPatchSet().uploader().equals(a.accountId())) {
+ allFormats.add(formatLabel(a.label(), a.value(), ChangeQueryBuilder.NON_UPLOADER_ACCOUNT_ID));
+ allFormats.addAll(
+ getMagicLabelFormats(
+ a.label(), a.value(), labelType, ChangeQueryBuilder.NON_UPLOADER_ACCOUNT_ID));
+ }
+ return allFormats;
+ }
+
public static Set<String> getAuthorParts(ChangeData cd) {
return SchemaUtil.getPersonParts(cd.getAuthor());
}
@@ -727,25 +792,33 @@
decodeProtos(field, PatchSetApprovalProtoConverter.INSTANCE)));
public static String formatLabel(String label, int value) {
- return formatLabel(label, value, null);
+ return formatLabel(label, value, /* accountId= */ null, /* count= */ null);
+ }
+
+ public static String formatLabel(String label, int value, @Nullable Integer count) {
+ return formatLabel(label, value, /* accountId= */ null, count);
}
public static String formatLabel(String label, int value, Account.Id accountId) {
+ return formatLabel(label, value, accountId, /* count= */ null);
+ }
+
+ public static String formatLabel(
+ String label, int value, @Nullable Account.Id accountId, @Nullable Integer count) {
return label.toLowerCase()
+ (value >= 0 ? "+" : "")
+ value
- + (accountId != null ? "," + formatAccount(accountId) : "");
+ + (accountId != null ? "," + formatAccount(accountId) : "")
+ + (count != null ? ",count=" + count : "");
}
- public static String formatLabel(String label, String value) {
- return formatLabel(label, value, null);
- }
-
- public static String formatLabel(String label, String value, @Nullable Account.Id accountId) {
+ public static String formatLabel(
+ String label, String value, @Nullable Account.Id accountId, @Nullable Integer count) {
return label.toLowerCase()
+ "="
+ value
- + (accountId != null ? "," + formatAccount(accountId) : "");
+ + (accountId != null ? "," + formatAccount(accountId) : "")
+ + (count != null ? ",count=" + count : "");
}
private static String formatAccount(Account.Id accountId) {
diff --git a/java/com/google/gerrit/server/index/change/ChangeSchemaDefinitions.java b/java/com/google/gerrit/server/index/change/ChangeSchemaDefinitions.java
index 9339d62..ee93065 100644
--- a/java/com/google/gerrit/server/index/change/ChangeSchemaDefinitions.java
+++ b/java/com/google/gerrit/server/index/change/ChangeSchemaDefinitions.java
@@ -183,9 +183,18 @@
new Schema.Builder<ChangeData>().add(V69).add(ChangeField.ATTENTION_SET_USERS_COUNT).build();
/** Added new field {@link ChangeField#UPLOADER}. */
+ @Deprecated
static final Schema<ChangeData> V71 =
new Schema.Builder<ChangeData>().add(V70).add(ChangeField.UPLOADER).build();
+ /** Added new field {@link ChangeField#IS_PURE_REVERT}. */
+ @Deprecated
+ static final Schema<ChangeData> V72 =
+ new Schema.Builder<ChangeData>().add(V71).add(ChangeField.IS_PURE_REVERT).build();
+
+ /** Added new "count=$count" argument to the {@link ChangeField#LABEL} operator. */
+ static final Schema<ChangeData> V73 = schema(V72, false);
+
/**
* Name of the change index to be used when contacting index backends or loading configurations.
*/
diff --git a/java/com/google/gerrit/server/mail/send/MailSoySauceProvider.java b/java/com/google/gerrit/server/mail/send/MailSoySauceLoader.java
similarity index 94%
rename from java/com/google/gerrit/server/mail/send/MailSoySauceProvider.java
rename to java/com/google/gerrit/server/mail/send/MailSoySauceLoader.java
index aade30f..ad1703d 100644
--- a/java/com/google/gerrit/server/mail/send/MailSoySauceProvider.java
+++ b/java/com/google/gerrit/server/mail/send/MailSoySauceLoader.java
@@ -19,7 +19,6 @@
import com.google.gerrit.server.config.SitePaths;
import com.google.gerrit.server.plugincontext.PluginSetContext;
import com.google.inject.Inject;
-import com.google.inject.Provider;
import com.google.inject.ProvisionException;
import com.google.inject.Singleton;
import com.google.template.soy.SoyFileSet;
@@ -31,9 +30,13 @@
import java.nio.file.Files;
import java.nio.file.Path;
-/** Configures Soy Sauce object for rendering email templates. */
+/**
+ * Configures and loads Soy Sauce object for rendering email templates.
+ *
+ * <p>It reloads templates each time when {@link #load()} is called.
+ */
@Singleton
-public class MailSoySauceProvider implements Provider<SoySauce> {
+class MailSoySauceLoader {
// Note: will fail to construct the tofu object if this array is empty.
private static final String[] TEMPLATES = {
@@ -90,7 +93,7 @@
private final PluginSetContext<MailSoyTemplateProvider> templateProviders;
@Inject
- MailSoySauceProvider(
+ MailSoySauceLoader(
SitePaths site,
SoyAstCache cache,
PluginSetContext<MailSoyTemplateProvider> templateProviders) {
@@ -99,8 +102,7 @@
this.templateProviders = templateProviders;
}
- @Override
- public SoySauce get() throws ProvisionException {
+ public SoySauce load() {
SoyFileSet.Builder builder = SoyFileSet.builder();
builder.setSoyAstCache(cache);
for (String name : TEMPLATES) {
diff --git a/java/com/google/gerrit/server/mail/send/MailSoySauceModule.java b/java/com/google/gerrit/server/mail/send/MailSoySauceModule.java
new file mode 100644
index 0000000..a3cf3e3
--- /dev/null
+++ b/java/com/google/gerrit/server/mail/send/MailSoySauceModule.java
@@ -0,0 +1,89 @@
+package com.google.gerrit.server.mail.send;
+
+import static com.google.common.base.Preconditions.checkArgument;
+
+import com.google.common.cache.CacheLoader;
+import com.google.common.cache.LoadingCache;
+import com.google.common.util.concurrent.ListenableFuture;
+import com.google.common.util.concurrent.ListeningExecutorService;
+import com.google.gerrit.server.CacheRefreshExecutor;
+import com.google.gerrit.server.cache.CacheModule;
+import com.google.inject.Inject;
+import com.google.inject.ProvisionException;
+import com.google.inject.Singleton;
+import com.google.inject.name.Named;
+import com.google.template.soy.jbcsrc.api.SoySauce;
+import java.time.Duration;
+import java.util.concurrent.ExecutionException;
+import javax.inject.Provider;
+
+/**
+ * Provides support for soy templates
+ *
+ * <p>Module loads templates with {@link MailSoySauceLoader} and caches compiled templates. The
+ * cache refreshes automatically, so Gerrit does not need to be restarted if templates are changed.
+ */
+public class MailSoySauceModule extends CacheModule {
+ static final String CACHE_NAME = "soy_sauce_compiled_templates";
+ private static final String SOY_LOADING_CACHE_KEY = "KEY";
+
+ @Override
+ protected void configure() {
+ // Cache stores only a single key-value pair (key is SOY_LOADING_CACHE_KEY). We are using
+ // cache only for it refresh/expire logic.
+ cache(CACHE_NAME, String.class, SoySauce.class)
+ // Cache refreshes a value only on the access (if refreshAfterWrite interval is
+ // passed). While the value is refreshed, cache returns old value.
+ // Adding expireAfterWrite interval prevents cache from returning very old template.
+ .refreshAfterWrite(Duration.ofSeconds(5))
+ .expireAfterWrite(Duration.ofMinutes(1))
+ .loader(SoySauceCacheLoader.class);
+ bind(SoySauce.class).annotatedWith(MailTemplates.class).toProvider(SoySauceProvider.class);
+ }
+
+ @Singleton
+ static class SoySauceProvider implements Provider<SoySauce> {
+ private final LoadingCache<String, SoySauce> templateCache;
+
+ @Inject
+ SoySauceProvider(@Named(CACHE_NAME) LoadingCache<String, SoySauce> templateCache) {
+ this.templateCache = templateCache;
+ }
+
+ @Override
+ public SoySauce get() {
+ try {
+ return templateCache.get(SOY_LOADING_CACHE_KEY);
+ } catch (ExecutionException e) {
+ throw new ProvisionException("Can't get SoySauce from the cache", e);
+ }
+ }
+ }
+
+ @Singleton
+ static class SoySauceCacheLoader extends CacheLoader<String, SoySauce> {
+ private final ListeningExecutorService executor;
+ private final MailSoySauceLoader loader;
+
+ @Inject
+ SoySauceCacheLoader(
+ @CacheRefreshExecutor ListeningExecutorService executor, MailSoySauceLoader loader) {
+ this.executor = executor;
+ this.loader = loader;
+ }
+
+ @Override
+ public SoySauce load(String key) throws Exception {
+ checkArgument(
+ SOY_LOADING_CACHE_KEY.equals(key),
+ "Cache can have only one element with a key '%s'",
+ SOY_LOADING_CACHE_KEY);
+ return loader.load();
+ }
+
+ @Override
+ public ListenableFuture<SoySauce> reload(String key, SoySauce soySauce) {
+ return executor.submit(() -> loader.load());
+ }
+ }
+}
diff --git a/java/com/google/gerrit/server/query/change/ChangePredicates.java b/java/com/google/gerrit/server/query/change/ChangePredicates.java
index 3afdcdd..7e50c6f 100644
--- a/java/com/google/gerrit/server/query/change/ChangePredicates.java
+++ b/java/com/google/gerrit/server/query/change/ChangePredicates.java
@@ -342,4 +342,12 @@
public static Predicate<ChangeData> submitRuleStatus(String value) {
return new ChangeIndexPredicate(ChangeField.SUBMIT_RULE_RESULT, value);
}
+
+ /**
+ * Returns a predicate that matches with changes that are pure reverts if {@code value} is equal
+ * to "1", or non-pure reverts if {@code value} is "0".
+ */
+ public static Predicate<ChangeData> pureRevert(String value) {
+ return new ChangeIndexPredicate(ChangeField.IS_PURE_REVERT, value);
+ }
}
diff --git a/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java b/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java
index 57191c5..d435df1 100644
--- a/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java
+++ b/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java
@@ -43,6 +43,7 @@
import com.google.gerrit.exceptions.NotSignedInException;
import com.google.gerrit.exceptions.StorageException;
import com.google.gerrit.extensions.registration.DynamicMap;
+import com.google.gerrit.index.FieldDef;
import com.google.gerrit.index.IndexConfig;
import com.google.gerrit.index.Schema;
import com.google.gerrit.index.SchemaUtil;
@@ -83,6 +84,7 @@
import com.google.gerrit.server.plugincontext.PluginSetContext;
import com.google.gerrit.server.project.ChildProjects;
import com.google.gerrit.server.project.ProjectCache;
+import com.google.gerrit.server.query.change.PredicateArgs.ValOp;
import com.google.gerrit.server.rules.SubmitRule;
import com.google.gerrit.server.submit.SubmitDryRun;
import com.google.inject.Inject;
@@ -203,6 +205,7 @@
public static final String FIELD_WATCHEDBY = "watchedby";
public static final String FIELD_WIP = "wip";
public static final String FIELD_REVERTOF = "revertof";
+ public static final String FIELD_PURE_REVERT = "ispurerevert";
public static final String FIELD_CHERRYPICK = "cherrypick";
public static final String FIELD_CHERRY_PICK_OF_CHANGE = "cherrypickofchange";
public static final String FIELD_CHERRY_PICK_OF_PATCHSET = "cherrypickofpatchset";
@@ -212,6 +215,7 @@
public static final String ARG_ID_GROUP = "group";
public static final String ARG_ID_OWNER = "owner";
public static final String ARG_ID_NON_UPLOADER = "non_uploader";
+ public static final String ARG_COUNT = "count";
public static final Account.Id OWNER_ACCOUNT_ID = Account.id(0);
public static final Account.Id NON_UPLOADER_ACCOUNT_ID = Account.id(-1);
@@ -521,22 +525,14 @@
@Operator
public Predicate<ChangeData> mergedBefore(String value) throws QueryParseException {
- if (!args.index.getSchema().hasField(ChangeField.MERGED_ON)) {
- throw new QueryParseException(
- String.format(
- "'%s' operator is not supported by change index version", OPERATOR_MERGED_BEFORE));
- }
+ checkFieldAvailable(ChangeField.MERGED_ON, OPERATOR_MERGED_BEFORE);
return new BeforePredicate(
ChangeField.MERGED_ON, ChangeQueryBuilder.OPERATOR_MERGED_BEFORE, value);
}
@Operator
public Predicate<ChangeData> mergedAfter(String value) throws QueryParseException {
- if (!args.index.getSchema().hasField(ChangeField.MERGED_ON)) {
- throw new QueryParseException(
- String.format(
- "'%s' operator is not supported by change index version", OPERATOR_MERGED_AFTER));
- }
+ checkFieldAvailable(ChangeField.MERGED_ON, OPERATOR_MERGED_AFTER);
return new AfterPredicate(
ChangeField.MERGED_ON, ChangeQueryBuilder.OPERATOR_MERGED_AFTER, value);
}
@@ -626,10 +622,7 @@
}
if ("attention".equalsIgnoreCase(value)) {
- if (!args.index.getSchema().hasField(ChangeField.ATTENTION_SET_USERS)) {
- throw new QueryParseException(
- "'has:attention' operator is not supported by change index version");
- }
+ checkFieldAvailable(ChangeField.ATTENTION_SET_USERS, "has:attention");
return new IsAttentionPredicate();
}
@@ -672,20 +665,13 @@
}
if ("uploader".equalsIgnoreCase(value)) {
- if (!args.getSchema().hasField(ChangeField.UPLOADER)) {
- throw new QueryParseException(
- "'is:uploader' operator is not supported by change index version");
- }
+ checkFieldAvailable(ChangeField.UPLOADER, "is:uploader");
return ChangePredicates.uploader(self());
}
if ("reviewer".equalsIgnoreCase(value)) {
- if (args.getSchema().hasField(ChangeField.WIP)) {
- return Predicate.and(
- Predicate.not(new BooleanPredicate(ChangeField.WIP)),
- ReviewerPredicate.reviewer(self()));
- }
- return ReviewerPredicate.reviewer(self());
+ return Predicate.and(
+ Predicate.not(new BooleanPredicate(ChangeField.WIP)), ReviewerPredicate.reviewer(self()));
}
if ("cc".equalsIgnoreCase(value)) {
@@ -700,25 +686,16 @@
}
if ("merge".equalsIgnoreCase(value)) {
- if (args.getSchema().hasField(ChangeField.MERGE)) {
- return new BooleanPredicate(ChangeField.MERGE);
- }
- throw new QueryParseException("'is:merge' operator is not supported by change index version");
+ checkFieldAvailable(ChangeField.MERGE, "is:merge");
+ return new BooleanPredicate(ChangeField.MERGE);
}
if ("private".equalsIgnoreCase(value)) {
- if (args.getSchema().hasField(ChangeField.PRIVATE)) {
- return new BooleanPredicate(ChangeField.PRIVATE);
- }
- throw new QueryParseException(
- "'is:private' operator is not supported by change index version");
+ return new BooleanPredicate(ChangeField.PRIVATE);
}
if ("attention".equalsIgnoreCase(value)) {
- if (!args.index.getSchema().hasField(ChangeField.ATTENTION_SET_USERS)) {
- throw new QueryParseException(
- "'is:attention' operator is not supported by change index version");
- }
+ checkFieldAvailable(ChangeField.ATTENTION_SET_USERS, "is:attention");
return new IsAttentionPredicate();
}
@@ -730,6 +707,11 @@
return ChangePredicates.assignee(Account.id(ChangeField.NO_ASSIGNEE));
}
+ if ("pure-revert".equalsIgnoreCase(value)) {
+ checkFieldAvailable(ChangeField.IS_PURE_REVERT, "is:pure-revert");
+ return ChangePredicates.pureRevert("1");
+ }
+
if ("submittable".equalsIgnoreCase(value)) {
// SubmittablePredicate will match if *any* of the submit records are OK,
// but we need to check that they're *all* OK, so check that none of the
@@ -747,26 +729,17 @@
}
if ("started".equalsIgnoreCase(value)) {
- if (args.getSchema().hasField(ChangeField.STARTED)) {
- return new BooleanPredicate(ChangeField.STARTED);
- }
- throw new QueryParseException(
- "'is:started' operator is not supported by change index version");
+ checkFieldAvailable(ChangeField.STARTED, "is:started");
+ return new BooleanPredicate(ChangeField.STARTED);
}
if ("wip".equalsIgnoreCase(value)) {
- if (args.getSchema().hasField(ChangeField.WIP)) {
- return new BooleanPredicate(ChangeField.WIP);
- }
- throw new QueryParseException("'is:wip' operator is not supported by change index version");
+ return new BooleanPredicate(ChangeField.WIP);
}
if ("cherrypick".equalsIgnoreCase(value)) {
- if (args.getSchema().hasField(ChangeField.CHERRY_PICK)) {
- return new BooleanPredicate(ChangeField.CHERRY_PICK);
- }
- throw new QueryParseException(
- "'is:cherrypick' operator is not supported by change index version");
+ checkFieldAvailable(ChangeField.CHERRY_PICK, "is:cherrypick");
+ return new BooleanPredicate(ChangeField.CHERRY_PICK);
}
// for plugins the value will be operandName_pluginName
@@ -883,10 +856,7 @@
return ChangePredicates.hashtag(hashtag);
}
- if (!args.index.getSchema().hasField(ChangeField.FUZZY_HASHTAG)) {
- throw new QueryParseException(
- "'inhashtag' operator is not supported by change index version");
- }
+ checkFieldAvailable(ChangeField.FUZZY_HASHTAG, "inhashtag");
return ChangePredicates.fuzzyHashtag(hashtag);
}
@@ -942,10 +912,7 @@
@Operator
public Predicate<ChangeData> extension(String ext) throws QueryParseException {
- if (args.getSchema().hasField(ChangeField.EXTENSION)) {
- return new FileExtensionPredicate(ext);
- }
- throw new QueryParseException("'extension' operator is not supported by change index version");
+ return new FileExtensionPredicate(ext);
}
@Operator
@@ -955,19 +922,12 @@
@Operator
public Predicate<ChangeData> onlyextensions(String extList) throws QueryParseException {
- if (args.getSchema().hasField(ChangeField.ONLY_EXTENSIONS)) {
- return new FileExtensionListPredicate(extList);
- }
- throw new QueryParseException(
- "'onlyextensions' operator is not supported by change index version");
+ return new FileExtensionListPredicate(extList);
}
@Operator
public Predicate<ChangeData> footer(String footer) throws QueryParseException {
- if (args.getSchema().hasField(ChangeField.FOOTER)) {
- return ChangePredicates.footer(footer);
- }
- throw new QueryParseException("'footer' operator is not supported by change index version");
+ return ChangePredicates.footer(footer);
}
@Operator
@@ -977,13 +937,10 @@
@Operator
public Predicate<ChangeData> directory(String directory) throws QueryParseException {
- if (args.getSchema().hasField(ChangeField.DIRECTORY)) {
- if (directory.startsWith("^")) {
- return new RegexDirectoryPredicate(directory);
- }
- return ChangePredicates.directory(directory);
+ if (directory.startsWith("^")) {
+ return new RegexDirectoryPredicate(directory);
}
- throw new QueryParseException("'directory' operator is not supported by change index version");
+ return ChangePredicates.directory(directory);
}
@Operator
@@ -991,6 +948,8 @@
throws QueryParseException, IOException, ConfigInvalidException {
Set<Account.Id> accounts = null;
AccountGroup.UUID group = null;
+ Integer count = null;
+ PredicateArgs.Operator countOp = null;
// Parse for:
// label:Code-Review=1,user=jsmith or
@@ -1001,6 +960,7 @@
// Special case: votes by owners can be tracked with ",owner":
// label:Code-Review+2,owner
// label:Code-Review+2,user=owner
+ // label:Code-Review+1,count=2
List<String> splitReviewer = Lists.newArrayList(Splitter.on(',').limit(2).split(name));
name = splitReviewer.get(0); // remove all but the vote piece, e.g.'CodeReview=1'
@@ -1008,17 +968,40 @@
// process the user/group piece
PredicateArgs lblArgs = new PredicateArgs(splitReviewer.get(1));
- for (Map.Entry<String, String> pair : lblArgs.keyValue.entrySet()) {
- if (pair.getKey().equalsIgnoreCase(ARG_ID_USER)) {
- if (pair.getValue().equals(ARG_ID_OWNER)) {
+ // Disallow using the "count=" arg in conjunction with the "user=" or "group=" args. to avoid
+ // unnecessary complexity.
+ assertDisjunctive(lblArgs, ARG_COUNT, ARG_ID_USER);
+ assertDisjunctive(lblArgs, ARG_COUNT, ARG_ID_GROUP);
+
+ for (Map.Entry<String, ValOp> pair : lblArgs.keyValue.entrySet()) {
+ String key = pair.getKey();
+ String value = pair.getValue().value();
+ PredicateArgs.Operator operator = pair.getValue().operator();
+ if (key.equalsIgnoreCase(ARG_ID_USER)) {
+ if (value.equals(ARG_ID_OWNER)) {
accounts = Collections.singleton(OWNER_ACCOUNT_ID);
- } else if (pair.getValue().equals(ARG_ID_NON_UPLOADER)) {
+ } else if (value.equals(ARG_ID_NON_UPLOADER)) {
accounts = Collections.singleton(NON_UPLOADER_ACCOUNT_ID);
} else {
- accounts = parseAccount(pair.getValue());
+ accounts = parseAccount(value);
}
- } else if (pair.getKey().equalsIgnoreCase(ARG_ID_GROUP)) {
- group = parseGroup(pair.getValue()).getUUID();
+ } else if (key.equalsIgnoreCase(ARG_ID_GROUP)) {
+ group = parseGroup(value).getUUID();
+ } else if (key.equalsIgnoreCase(ARG_COUNT)) {
+ if (!isInt(value)) {
+ throw new QueryParseException("Invalid count argument. Value should be an integer");
+ }
+ count = Integer.parseInt(value);
+ countOp = operator;
+ if (count == 0) {
+ throw new QueryParseException("Argument count=0 is not allowed.");
+ }
+ if (count > LabelPredicate.MAX_COUNT) {
+ throw new QueryParseException(
+ String.format(
+ "count=%d is not allowed. Maximum allowed value for count is %d.",
+ count, LabelPredicate.MAX_COUNT));
+ }
} else {
throw new QueryParseException("Invalid argument identifier '" + pair.getKey() + "'");
}
@@ -1055,7 +1038,7 @@
// If the vote piece looks like Code-Review=NEED with a valid non-numeric
// submit record status, interpret as a submit record query.
int eq = name.indexOf('=');
- if (args.getSchema().hasField(ChangeField.SUBMIT_RECORD) && eq > 0) {
+ if (eq > 0) {
String statusName = name.substring(eq + 1).toUpperCase();
if (!isInt(statusName) && !MagicLabelValue.tryParse(statusName).isPresent()) {
SubmitRecord.Label.Status status =
@@ -1067,7 +1050,18 @@
}
}
- return new LabelPredicate(args, name, accounts, group);
+ return new LabelPredicate(args, name, accounts, group, count, countOp);
+ }
+
+ /** Assert that keys {@code k1} and {@code k2} do not exist in {@code labelArgs} together. */
+ private void assertDisjunctive(PredicateArgs labelArgs, String k1, String k2)
+ throws QueryParseException {
+ Map<String, ValOp> keyValArgs = labelArgs.keyValue;
+ if (keyValArgs.containsKey(k1) && keyValArgs.containsKey(k2)) {
+ throw new QueryParseException(
+ String.format(
+ "Cannot use the '%s' argument in conjunction with the '%s' argument", k1, k2));
+ }
}
private static boolean isInt(String s) {
@@ -1197,9 +1191,7 @@
@Operator
public Predicate<ChangeData> uploader(String who)
throws QueryParseException, IOException, ConfigInvalidException {
- if (!args.getSchema().hasField(ChangeField.UPLOADER)) {
- throw new QueryParseException("'uploader' operator is not supported by change index version");
- }
+ checkFieldAvailable(ChangeField.UPLOADER, "uploader");
return uploader(parseAccount(who, (AccountState s) -> true));
}
@@ -1214,10 +1206,7 @@
@Operator
public Predicate<ChangeData> attention(String who)
throws QueryParseException, IOException, ConfigInvalidException {
- if (!args.index.getSchema().hasField(ChangeField.ATTENTION_SET_USERS)) {
- throw new QueryParseException(
- "'attention' operator is not supported by change index version");
- }
+ checkFieldAvailable(ChangeField.ATTENTION_SET_USERS, "attention");
return attention(parseAccount(who, (AccountState s) -> true));
}
@@ -1262,9 +1251,7 @@
@Operator
public Predicate<ChangeData> uploaderin(String group) throws QueryParseException, IOException {
- if (!args.getSchema().hasField(ChangeField.UPLOADER)) {
- throw new QueryParseException("'uploader' operator is not supported by change index version");
- }
+ checkFieldAvailable(ChangeField.UPLOADER, "uploaderin");
GroupReference g = GroupBackends.findBestSuggestion(args.groupBackend, group);
if (g == null) {
@@ -1309,10 +1296,7 @@
if (Objects.equals(byState, Predicate.<ChangeData>any())) {
return Predicate.any();
}
- if (args.getSchema().hasField(ChangeField.WIP)) {
- return Predicate.and(Predicate.not(new BooleanPredicate(ChangeField.WIP)), byState);
- }
- return byState;
+ return Predicate.and(Predicate.not(new BooleanPredicate(ChangeField.WIP)), byState);
}
@Operator
@@ -1400,7 +1384,7 @@
try (Repository git = args.repoManager.openRepository(args.allUsersName)) {
// [name=]<name>
if (inputArgs.keyValue.containsKey(ARG_ID_NAME)) {
- name = inputArgs.keyValue.get(ARG_ID_NAME);
+ name = inputArgs.keyValue.get(ARG_ID_NAME).value();
} else if (inputArgs.positional.size() == 1) {
name = Iterables.getOnlyElement(inputArgs.positional);
} else if (inputArgs.positional.size() > 1) {
@@ -1409,7 +1393,7 @@
// [,user=<user>]
if (inputArgs.keyValue.containsKey(ARG_ID_USER)) {
- Set<Account.Id> accounts = parseAccount(inputArgs.keyValue.get(ARG_ID_USER));
+ Set<Account.Id> accounts = parseAccount(inputArgs.keyValue.get(ARG_ID_USER).value());
if (accounts != null && accounts.size() > 1) {
throw error(
String.format(
@@ -1451,7 +1435,7 @@
try (Repository git = args.repoManager.openRepository(args.allUsersName)) {
// [name=]<name>
if (inputArgs.keyValue.containsKey(ARG_ID_NAME)) {
- name = inputArgs.keyValue.get(ARG_ID_NAME);
+ name = inputArgs.keyValue.get(ARG_ID_NAME).value();
} else if (inputArgs.positional.size() == 1) {
name = Iterables.getOnlyElement(inputArgs.positional);
} else if (inputArgs.positional.size() > 1) {
@@ -1460,7 +1444,7 @@
// [,user=<user>]
if (inputArgs.keyValue.containsKey(ARG_ID_USER)) {
- Set<Account.Id> accounts = parseAccount(inputArgs.keyValue.get(ARG_ID_USER));
+ Set<Account.Id> accounts = parseAccount(inputArgs.keyValue.get(ARG_ID_USER).value());
if (accounts != null && accounts.size() > 1) {
throw error(
String.format(
@@ -1488,20 +1472,14 @@
@Operator
public Predicate<ChangeData> author(String who) throws QueryParseException {
- if (args.getSchema().hasField(ChangeField.EXACT_AUTHOR)) {
- return getAuthorOrCommitterPredicate(
- who.trim(), ChangePredicates::exactAuthor, ChangePredicates::author);
- }
- return getAuthorOrCommitterFullTextPredicate(who.trim(), ChangePredicates::author);
+ return getAuthorOrCommitterPredicate(
+ who.trim(), ChangePredicates::exactAuthor, ChangePredicates::author);
}
@Operator
public Predicate<ChangeData> committer(String who) throws QueryParseException {
- if (args.getSchema().hasField(ChangeField.EXACT_COMMITTER)) {
- return getAuthorOrCommitterPredicate(
- who.trim(), ChangePredicates::exactCommitter, ChangePredicates::committer);
- }
- return getAuthorOrCommitterFullTextPredicate(who.trim(), ChangePredicates::committer);
+ return getAuthorOrCommitterPredicate(
+ who.trim(), ChangePredicates::exactCommitter, ChangePredicates::committer);
}
@Operator
@@ -1524,41 +1502,31 @@
if (value == null || Ints.tryParse(value) == null) {
throw new QueryParseException("'revertof' must be an integer");
}
- if (args.getSchema().hasField(ChangeField.REVERT_OF)) {
- return ChangePredicates.revertOf(Change.id(Ints.tryParse(value)));
- }
- throw new QueryParseException("'revertof' operator is not supported by change index version");
+ return ChangePredicates.revertOf(Change.id(Ints.tryParse(value)));
}
@Operator
public Predicate<ChangeData> submissionId(String value) throws QueryParseException {
- if (args.getSchema().hasField(ChangeField.SUBMISSIONID)) {
- return ChangePredicates.submissionId(value);
- }
- throw new QueryParseException(
- "'submissionid' operator is not supported by change index version");
+ return ChangePredicates.submissionId(value);
}
@Operator
public Predicate<ChangeData> cherryPickOf(String value) throws QueryParseException {
- if (args.getSchema().hasField(ChangeField.CHERRY_PICK_OF_CHANGE)
- && args.getSchema().hasField(ChangeField.CHERRY_PICK_OF_PATCHSET)) {
- if (Ints.tryParse(value) != null) {
- return ChangePredicates.cherryPickOf(Change.id(Ints.tryParse(value)));
- }
- try {
- PatchSet.Id patchSetId = PatchSet.Id.parse(value);
- return ChangePredicates.cherryPickOf(patchSetId);
- } catch (IllegalArgumentException e) {
- throw new QueryParseException(
- "'"
- + value
- + "' is not a valid input. It must be in the 'ChangeNumber[,PatchsetNumber]' format.",
- e);
- }
+ checkFieldAvailable(ChangeField.CHERRY_PICK_OF_CHANGE, "cherryPickOf");
+ checkFieldAvailable(ChangeField.CHERRY_PICK_OF_PATCHSET, "cherryPickOf");
+ if (Ints.tryParse(value) != null) {
+ return ChangePredicates.cherryPickOf(Change.id(Ints.tryParse(value)));
}
- throw new QueryParseException(
- "'cherrypickof' operator is not supported by change index version");
+ try {
+ PatchSet.Id patchSetId = PatchSet.Id.parse(value);
+ return ChangePredicates.cherryPickOf(patchSetId);
+ } catch (IllegalArgumentException e) {
+ throw new QueryParseException(
+ "'"
+ + value
+ + "' is not a valid input. It must be in the 'ChangeNumber[,PatchsetNumber]' format.",
+ e);
+ }
}
@Override
@@ -1617,6 +1585,14 @@
return Predicate.or(predicates);
}
+ protected void checkFieldAvailable(FieldDef<ChangeData, ?> field, String operator)
+ throws QueryParseException {
+ if (!args.index.getSchema().hasField(field)) {
+ throw new QueryParseException(
+ String.format("'%s' operator is not supported by change index version", operator));
+ }
+ }
+
private Predicate<ChangeData> getAuthorOrCommitterPredicate(
String who,
Function<String, Predicate<ChangeData>> exactPredicateFunc,
@@ -1731,11 +1707,9 @@
String who, ReviewerStateInternal state, boolean forDefaultField)
throws QueryParseException, IOException, ConfigInvalidException {
Predicate<ChangeData> reviewerByEmailPredicate = null;
- if (args.index.getSchema().hasField(ChangeField.REVIEWER_BY_EMAIL)) {
- Address address = Address.tryParse(who);
- if (address != null) {
- reviewerByEmailPredicate = ReviewerByEmailPredicate.forState(address, state);
- }
+ Address address = Address.tryParse(who);
+ if (address != null) {
+ reviewerByEmailPredicate = ReviewerByEmailPredicate.forState(address, state);
}
Predicate<ChangeData> reviewerPredicate = null;
diff --git a/java/com/google/gerrit/server/query/change/EqualsLabelPredicate.java b/java/com/google/gerrit/server/query/change/EqualsLabelPredicate.java
index 12efecb..b2bc6aa 100644
--- a/java/com/google/gerrit/server/query/change/EqualsLabelPredicate.java
+++ b/java/com/google/gerrit/server/query/change/EqualsLabelPredicate.java
@@ -14,6 +14,7 @@
package com.google.gerrit.server.query.change;
+import com.google.gerrit.common.Nullable;
import com.google.gerrit.entities.Account;
import com.google.gerrit.entities.AccountGroup;
import com.google.gerrit.entities.Change;
@@ -34,17 +35,34 @@
protected final ProjectCache projectCache;
protected final PermissionBackend permissionBackend;
protected final IdentifiedUser.GenericFactory userFactory;
+ /** label name to be matched. */
protected final String label;
+
+ /** Expected vote value for the label. */
protected final int expVal;
+
+ /**
+ * Number of times the value {@link #expVal} for label {@link #label} should occur. If null, match
+ * with any count greater or equal to 1.
+ */
+ @Nullable protected final Integer count;
+
+ /** Account ID that has voted on the label. */
protected final Account.Id account;
+
protected final AccountGroup.UUID group;
public EqualsLabelPredicate(
- LabelPredicate.Args args, String label, int expVal, Account.Id account) {
- super(ChangeField.LABEL, ChangeField.formatLabel(label, expVal, account));
+ LabelPredicate.Args args,
+ String label,
+ int expVal,
+ Account.Id account,
+ @Nullable Integer count) {
+ super(ChangeField.LABEL, ChangeField.formatLabel(label, expVal, account, count));
this.permissionBackend = args.permissionBackend;
this.projectCache = args.projectCache;
this.userFactory = args.userFactory;
+ this.count = count;
this.group = args.group;
this.label = label;
this.expVal = expVal;
@@ -60,6 +78,14 @@
return false;
}
+ if (Integer.valueOf(0).equals(count)) {
+ // We don't match against count=0 so that the computation is identical to the stored values
+ // in the index. We do that since computing count=0 requires looping on all {label_type,
+ // vote_value} for the change and storing a {count=0} format for it in the change index which
+ // is computationally expensive.
+ return false;
+ }
+
Optional<ProjectState> project = projectCache.get(c.getDest().project());
if (!project.isPresent()) {
// The project has disappeared.
@@ -73,12 +99,13 @@
}
boolean hasVote = false;
+ int matchingVotes = 0;
object.setStorageConstraint(ChangeData.StorageConstraint.INDEX_PRIMARY_NOTEDB_SECONDARY);
for (PatchSetApproval p : object.currentApprovals()) {
if (labelType.matches(p)) {
hasVote = true;
if (match(object, p.value(), p.accountId())) {
- return true;
+ matchingVotes += 1;
}
}
}
@@ -87,7 +114,7 @@
return true;
}
- return false;
+ return count == null ? matchingVotes >= 1 : matchingVotes == count;
}
protected static LabelType type(LabelTypes types, String toFind) {
diff --git a/java/com/google/gerrit/server/query/change/LabelPredicate.java b/java/com/google/gerrit/server/query/change/LabelPredicate.java
index 5f017fb..2e09075 100644
--- a/java/com/google/gerrit/server/query/change/LabelPredicate.java
+++ b/java/com/google/gerrit/server/query/change/LabelPredicate.java
@@ -14,8 +14,8 @@
package com.google.gerrit.server.query.change;
-import com.google.common.collect.ImmutableList;
import com.google.common.collect.Lists;
+import com.google.gerrit.common.Nullable;
import com.google.gerrit.entities.Account;
import com.google.gerrit.entities.AccountGroup;
import com.google.gerrit.index.query.OrPredicate;
@@ -29,9 +29,11 @@
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
+import java.util.stream.IntStream;
public class LabelPredicate extends OrPredicate<ChangeData> {
protected static final int MAX_LABEL_VALUE = 4;
+ protected static final int MAX_COUNT = 5; // inclusive
protected static class Args {
protected final ProjectCache projectCache;
@@ -40,6 +42,8 @@
protected final String value;
protected final Set<Account.Id> accounts;
protected final AccountGroup.UUID group;
+ protected final Integer count;
+ protected final PredicateArgs.Operator countOp;
protected Args(
ProjectCache projectCache,
@@ -47,13 +51,17 @@
IdentifiedUser.GenericFactory userFactory,
String value,
Set<Account.Id> accounts,
- AccountGroup.UUID group) {
+ AccountGroup.UUID group,
+ @Nullable Integer count,
+ @Nullable PredicateArgs.Operator countOp) {
this.projectCache = projectCache;
this.permissionBackend = permissionBackend;
this.userFactory = userFactory;
this.value = value;
this.accounts = accounts;
this.group = group;
+ this.count = count;
+ this.countOp = countOp;
}
}
@@ -75,19 +83,35 @@
ChangeQueryBuilder.Arguments a,
String value,
Set<Account.Id> accounts,
- AccountGroup.UUID group) {
+ AccountGroup.UUID group,
+ @Nullable Integer count,
+ @Nullable PredicateArgs.Operator countOp) {
super(
predicates(
- new Args(a.projectCache, a.permissionBackend, a.userFactory, value, accounts, group)));
+ new Args(
+ a.projectCache,
+ a.permissionBackend,
+ a.userFactory,
+ value,
+ accounts,
+ group,
+ count,
+ countOp)));
this.value = value;
}
protected static List<Predicate<ChangeData>> predicates(Args args) {
String v = args.value;
-
+ List<Integer> counts = getCounts(args.count, args.countOp);
try {
MagicLabelVote mlv = MagicLabelVote.parseWithEquals(v);
- return ImmutableList.of(magicLabelPredicate(args, mlv));
+ List<Predicate<ChangeData>> result = Lists.newArrayListWithCapacity(counts.size());
+ if (counts.isEmpty()) {
+ result.add(magicLabelPredicate(args, mlv, /* count= */ null));
+ } else {
+ counts.forEach(count -> result.add(magicLabelPredicate(args, mlv, count)));
+ }
+ return result;
} catch (IllegalArgumentException e) {
// Try next format.
}
@@ -123,16 +147,24 @@
int min = range.min;
int max = range.max;
- List<Predicate<ChangeData>> r = Lists.newArrayListWithCapacity(max - min + 1);
+ List<Predicate<ChangeData>> r =
+ Lists.newArrayListWithCapacity((counts.isEmpty() ? 1 : counts.size()) * (max - min + 1));
for (int i = min; i <= max; i++) {
- r.add(onePredicate(args, prefix, i));
+ if (counts.isEmpty()) {
+ r.add(onePredicate(args, prefix, i, /* count= */ null));
+ } else {
+ for (int count : counts) {
+ r.add(onePredicate(args, prefix, i, count));
+ }
+ }
}
return r;
}
- protected static Predicate<ChangeData> onePredicate(Args args, String label, int expVal) {
+ protected static Predicate<ChangeData> onePredicate(
+ Args args, String label, int expVal, @Nullable Integer count) {
if (expVal != 0) {
- return equalsLabelPredicate(args, label, expVal);
+ return equalsLabelPredicate(args, label, expVal, count);
}
return noLabelQuery(args, label);
}
@@ -140,34 +172,66 @@
protected static Predicate<ChangeData> noLabelQuery(Args args, String label) {
List<Predicate<ChangeData>> r = Lists.newArrayListWithCapacity(2 * MAX_LABEL_VALUE);
for (int i = 1; i <= MAX_LABEL_VALUE; i++) {
- r.add(equalsLabelPredicate(args, label, i));
- r.add(equalsLabelPredicate(args, label, -i));
+ r.add(equalsLabelPredicate(args, label, i, /* count= */ null));
+ r.add(equalsLabelPredicate(args, label, -i, /* count= */ null));
}
return not(or(r));
}
- protected static Predicate<ChangeData> equalsLabelPredicate(Args args, String label, int expVal) {
+ protected static Predicate<ChangeData> equalsLabelPredicate(
+ Args args, String label, int expVal, @Nullable Integer count) {
if (args.accounts == null || args.accounts.isEmpty()) {
- return new EqualsLabelPredicate(args, label, expVal, null);
+ return new EqualsLabelPredicate(args, label, expVal, null, count);
}
List<Predicate<ChangeData>> r = new ArrayList<>();
for (Account.Id a : args.accounts) {
- r.add(new EqualsLabelPredicate(args, label, expVal, a));
+ r.add(new EqualsLabelPredicate(args, label, expVal, a, count));
}
return or(r);
}
- protected static Predicate<ChangeData> magicLabelPredicate(Args args, MagicLabelVote mlv) {
+ protected static Predicate<ChangeData> magicLabelPredicate(
+ Args args, MagicLabelVote mlv, @Nullable Integer count) {
if (args.accounts == null || args.accounts.isEmpty()) {
- return new MagicLabelPredicate(args, mlv, /* account= */ null);
+ return new MagicLabelPredicate(args, mlv, /* account= */ null, count);
}
List<Predicate<ChangeData>> r = new ArrayList<>();
for (Account.Id a : args.accounts) {
- r.add(new MagicLabelPredicate(args, mlv, a));
+ r.add(new MagicLabelPredicate(args, mlv, a, count));
}
return or(r);
}
+ private static List<Integer> getCounts(
+ @Nullable Integer count, @Nullable PredicateArgs.Operator countOp) {
+ List<Integer> result = new ArrayList<>();
+ if (count == null) {
+ return result;
+ }
+ switch (countOp) {
+ case EQUAL:
+ case GREATER_EQUAL:
+ case LESS_EQUAL:
+ result.add(count);
+ break;
+ default:
+ break;
+ }
+ switch (countOp) {
+ case GREATER:
+ case GREATER_EQUAL:
+ IntStream.range(count + 1, MAX_COUNT + 1).forEach(result::add);
+ break;
+ case LESS:
+ case LESS_EQUAL:
+ IntStream.range(0, count).forEach(result::add);
+ break;
+ default:
+ break;
+ }
+ return result;
+ }
+
@Override
public String toString() {
return ChangeQueryBuilder.FIELD_LABEL + ":" + value;
diff --git a/java/com/google/gerrit/server/query/change/MagicLabelPredicate.java b/java/com/google/gerrit/server/query/change/MagicLabelPredicate.java
index 3917c79..5a81ca1 100644
--- a/java/com/google/gerrit/server/query/change/MagicLabelPredicate.java
+++ b/java/com/google/gerrit/server/query/change/MagicLabelPredicate.java
@@ -14,6 +14,7 @@
package com.google.gerrit.server.query.change;
+import com.google.gerrit.common.Nullable;
import com.google.gerrit.entities.Account;
import com.google.gerrit.entities.Change;
import com.google.gerrit.entities.LabelType;
@@ -30,13 +31,21 @@
protected final LabelPredicate.Args args;
private final MagicLabelVote magicLabelVote;
private final Account.Id account;
+ @Nullable private final Integer count;
public MagicLabelPredicate(
- LabelPredicate.Args args, MagicLabelVote magicLabelVote, Account.Id account) {
- super(ChangeField.LABEL, magicLabelVote.formatLabel());
+ LabelPredicate.Args args,
+ MagicLabelVote magicLabelVote,
+ Account.Id account,
+ @Nullable Integer count) {
+ super(
+ ChangeField.LABEL,
+ ChangeField.formatLabel(
+ magicLabelVote.label(), magicLabelVote.value().name(), account, count));
this.account = account;
this.args = args;
this.magicLabelVote = magicLabelVote;
+ this.count = count;
}
@Override
@@ -87,7 +96,7 @@
}
private EqualsLabelPredicate numericPredicate(String label, short value) {
- return new EqualsLabelPredicate(args, label, value, account);
+ return new EqualsLabelPredicate(args, label, value, account, count);
}
protected static LabelType type(LabelTypes types, String toFind) {
diff --git a/java/com/google/gerrit/server/query/change/PredicateArgs.java b/java/com/google/gerrit/server/query/change/PredicateArgs.java
index d82b9bc..9f0dffb 100644
--- a/java/com/google/gerrit/server/query/change/PredicateArgs.java
+++ b/java/com/google/gerrit/server/query/change/PredicateArgs.java
@@ -14,12 +14,15 @@
package com.google.gerrit.server.query.change;
+import com.google.auto.value.AutoValue;
import com.google.common.base.Splitter;
import com.google.gerrit.index.query.QueryParseException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
/**
* This class is used to extract comma separated values in a predicate.
@@ -30,8 +33,35 @@
* appear in the map and others in the positional list (e.g. "vote=approved,jb_2.3).
*/
public class PredicateArgs {
+ private static final Pattern SPLIT_PATTERN = Pattern.compile("(>|>=|=|<|<=)([^=].*)$");
+
public List<String> positional;
- public Map<String, String> keyValue;
+ public Map<String, ValOp> keyValue;
+
+ enum Operator {
+ EQUAL("="),
+ GREATER_EQUAL(">="),
+ GREATER(">"),
+ LESS_EQUAL("<="),
+ LESS("<");
+
+ final String op;
+
+ Operator(String op) {
+ this.op = op;
+ }
+ };
+
+ @AutoValue
+ public abstract static class ValOp {
+ abstract String value();
+
+ abstract Operator operator();
+
+ static ValOp create(String value, Operator operator) {
+ return new AutoValue_PredicateArgs_ValOp(value, operator);
+ }
+ }
/**
* Parses query arguments into {@link #keyValue} and/or {@link #positional}..
@@ -46,19 +76,39 @@
keyValue = new HashMap<>();
for (String arg : Splitter.on(',').split(args)) {
- List<String> splitKeyValue = Splitter.on('=').splitToList(arg);
+ Matcher m = SPLIT_PATTERN.matcher(arg);
- if (splitKeyValue.size() == 1) {
- positional.add(splitKeyValue.get(0));
- } else if (splitKeyValue.size() == 2) {
- if (!keyValue.containsKey(splitKeyValue.get(0))) {
- keyValue.put(splitKeyValue.get(0), splitKeyValue.get(1));
+ if (!m.find()) {
+ positional.add(arg);
+ } else if (m.groupCount() == 2) {
+ String key = arg.substring(0, m.start());
+ String op = m.group(1);
+ String val = m.group(2);
+ if (!keyValue.containsKey(key)) {
+ keyValue.put(key, ValOp.create(val, getOperator(op)));
} else {
- throw new QueryParseException("Duplicate key " + splitKeyValue.get(0));
+ throw new QueryParseException("Duplicate key " + key);
}
} else {
- throw new QueryParseException("invalid arg " + arg);
+ throw new QueryParseException("Invalid arg " + arg);
}
}
}
+
+ private Operator getOperator(String operator) {
+ switch (operator) {
+ case "<":
+ return Operator.LESS;
+ case "<=":
+ return Operator.LESS_EQUAL;
+ case "=":
+ return Operator.EQUAL;
+ case ">=":
+ return Operator.GREATER_EQUAL;
+ case ">":
+ return Operator.GREATER;
+ default:
+ throw new IllegalArgumentException("Invalid Operator " + operator);
+ }
+ }
}
diff --git a/java/com/google/gerrit/server/query/change/SubmitRequirementChangeQueryBuilder.java b/java/com/google/gerrit/server/query/change/SubmitRequirementChangeQueryBuilder.java
index e63714f..cd0fee3 100644
--- a/java/com/google/gerrit/server/query/change/SubmitRequirementChangeQueryBuilder.java
+++ b/java/com/google/gerrit/server/query/change/SubmitRequirementChangeQueryBuilder.java
@@ -14,6 +14,7 @@
package com.google.gerrit.server.query.change;
+import com.google.gerrit.index.FieldDef;
import com.google.gerrit.index.query.QueryBuilder;
import com.google.inject.Inject;
@@ -32,4 +33,10 @@
SubmitRequirementChangeQueryBuilder(Arguments args) {
super(def, args);
}
+
+ @Override
+ protected void checkFieldAvailable(FieldDef<ChangeData, ?> field, String operator) {
+ // Submit requirements don't rely on the index, so they can be used regardless of index schema
+ // version.
+ }
}
diff --git a/java/com/google/gerrit/server/restapi/change/Abandon.java b/java/com/google/gerrit/server/restapi/change/Abandon.java
index 2cfc3f5..affe947 100644
--- a/java/com/google/gerrit/server/restapi/change/Abandon.java
+++ b/java/com/google/gerrit/server/restapi/change/Abandon.java
@@ -30,6 +30,7 @@
import com.google.gerrit.server.change.ChangeResource;
import com.google.gerrit.server.change.NotifyResolver;
import com.google.gerrit.server.notedb.ChangeNotes;
+import com.google.gerrit.server.notedb.StoreSubmitRequirementsOp;
import com.google.gerrit.server.permissions.ChangePermission;
import com.google.gerrit.server.permissions.PermissionBackendException;
import com.google.gerrit.server.update.BatchUpdate;
@@ -48,6 +49,7 @@
private final AbandonOp.Factory abandonOpFactory;
private final NotifyResolver notifyResolver;
private final PatchSetUtil patchSetUtil;
+ private final StoreSubmitRequirementsOp.Factory storeSubmitRequirementsOpFactory;
@Inject
Abandon(
@@ -55,12 +57,14 @@
ChangeJson.Factory json,
AbandonOp.Factory abandonOpFactory,
NotifyResolver notifyResolver,
- PatchSetUtil patchSetUtil) {
+ PatchSetUtil patchSetUtil,
+ StoreSubmitRequirementsOp.Factory storeSubmitRequirementsOpFactory) {
this.updateFactory = updateFactory;
this.json = json;
this.abandonOpFactory = abandonOpFactory;
this.notifyResolver = notifyResolver;
this.patchSetUtil = patchSetUtil;
+ this.storeSubmitRequirementsOpFactory = storeSubmitRequirementsOpFactory;
}
@Override
@@ -119,7 +123,9 @@
AbandonOp op = abandonOpFactory.create(accountState, msgTxt);
try (BatchUpdate u = updateFactory.create(notes.getProjectName(), user, TimeUtil.nowTs())) {
u.setNotify(notify);
- u.addOp(notes.getChangeId(), op).execute();
+ u.addOp(notes.getChangeId(), op);
+ u.addOp(notes.getChangeId(), storeSubmitRequirementsOpFactory.create());
+ u.execute();
}
return op.getChange();
}
diff --git a/javatests/com/google/gerrit/acceptance/api/change/ChangeIT.java b/javatests/com/google/gerrit/acceptance/api/change/ChangeIT.java
index 50098d7..58c222a 100644
--- a/javatests/com/google/gerrit/acceptance/api/change/ChangeIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/change/ChangeIT.java
@@ -4644,6 +4644,131 @@
ExperimentFeaturesConstants
.GERRIT_BACKEND_REQUEST_FEATURE_STORE_SUBMIT_REQUIREMENTS_ON_MERGE
})
+ public void submitRequirement_storedForAbandonedChanges() throws Exception {
+ for (SubmitType submitType : SubmitType.values()) {
+ Project.NameKey project = createProjectForPush(submitType);
+ TestRepository<InMemoryRepository> repo = cloneProject(project);
+ configSubmitRequirement(
+ project,
+ SubmitRequirement.builder()
+ .setName("Code-Review")
+ .setSubmittabilityExpression(
+ SubmitRequirementExpression.create("label:Code-Review=+2"))
+ .setAllowOverrideInChildProjects(false)
+ .build());
+
+ PushOneCommit.Result r =
+ createChange(repo, "master", "Add a file", "foo", "content", "topic");
+ String changeId = r.getChangeId();
+
+ voteLabel(changeId, "Code-Review", 2);
+ ChangeInfo change = gApi.changes().id(changeId).get();
+ assertThat(change.submitRequirements).hasSize(1);
+ assertSubmitRequirementStatus(
+ change.submitRequirements, "Code-Review", Status.SATISFIED, /* isLegacy= */ false);
+
+ gApi.changes().id(r.getChangeId()).abandon();
+ ChangeNotes notes = notesFactory.create(project, r.getChange().getId());
+ SubmitRequirementResult result =
+ notes.getSubmitRequirementsResult().stream().collect(MoreCollectors.onlyElement());
+ assertThat(result.status()).isEqualTo(SubmitRequirementResult.Status.SATISFIED);
+ assertThat(result.submittabilityExpressionResult().status())
+ .isEqualTo(SubmitRequirementExpressionResult.Status.PASS);
+ assertThat(result.submittabilityExpressionResult().expression().expressionString())
+ .isEqualTo("label:Code-Review=+2");
+ }
+ }
+
+ @Test
+ @GerritConfig(
+ name = "experiments.enabled",
+ values = {
+ ExperimentFeaturesConstants.GERRIT_BACKEND_REQUEST_FEATURE_ENABLE_SUBMIT_REQUIREMENTS,
+ ExperimentFeaturesConstants
+ .GERRIT_BACKEND_REQUEST_FEATURE_STORE_SUBMIT_REQUIREMENTS_ON_MERGE
+ })
+ public void submitRequirement_retrievedFromNoteDbForAbandonedChanges() throws Exception {
+ for (SubmitType submitType : SubmitType.values()) {
+ Project.NameKey project = createProjectForPush(submitType);
+ TestRepository<InMemoryRepository> repo = cloneProject(project);
+ configSubmitRequirement(
+ project,
+ SubmitRequirement.builder()
+ .setName("Code-Review")
+ .setSubmittabilityExpression(
+ SubmitRequirementExpression.create("label:Code-Review=+2"))
+ .setAllowOverrideInChildProjects(false)
+ .build());
+
+ PushOneCommit.Result r =
+ createChange(repo, "master", "Add a file", "foo", "content", "topic");
+ String changeId = r.getChangeId();
+ voteLabel(changeId, "Code-Review", 2);
+ gApi.changes().id(changeId).abandon();
+
+ // Add another submit requirement. This will not get returned for the abandoned change, since
+ // we return the state of the SR results when the change was abandoned.
+ configSubmitRequirement(
+ project,
+ SubmitRequirement.builder()
+ .setName("New-Requirement")
+ .setSubmittabilityExpression(SubmitRequirementExpression.create("-has:unresolved"))
+ .setAllowOverrideInChildProjects(false)
+ .build());
+ ChangeInfo changeInfo =
+ gApi.changes().id(changeId).get(ListChangesOption.SUBMIT_REQUIREMENTS);
+ assertThat(changeInfo.submitRequirements).hasSize(1);
+ assertSubmitRequirementStatus(
+ changeInfo.submitRequirements,
+ "Code-Review",
+ Status.SATISFIED,
+ /* isLegacy= */ false,
+ /* submittabilityCondition= */ "label:Code-Review=+2");
+
+ // Restore the change, the new requirement will show up
+ gApi.changes().id(changeId).restore();
+ changeInfo = gApi.changes().id(changeId).get(ListChangesOption.SUBMIT_REQUIREMENTS);
+ assertThat(changeInfo.submitRequirements).hasSize(2);
+ assertSubmitRequirementStatus(
+ changeInfo.submitRequirements,
+ "Code-Review",
+ Status.SATISFIED,
+ /* isLegacy= */ false,
+ /* submittabilityCondition= */ "label:Code-Review=+2");
+ assertSubmitRequirementStatus(
+ changeInfo.submitRequirements,
+ "New-Requirement",
+ Status.SATISFIED,
+ /* isLegacy= */ false,
+ /* submittabilityCondition= */ "-has:unresolved");
+
+ // Abandon again, make sure the new requirement was persisted
+ gApi.changes().id(changeId).abandon();
+ changeInfo = gApi.changes().id(changeId).get(ListChangesOption.SUBMIT_REQUIREMENTS);
+ assertThat(changeInfo.submitRequirements).hasSize(2);
+ assertSubmitRequirementStatus(
+ changeInfo.submitRequirements,
+ "Code-Review",
+ Status.SATISFIED,
+ /* isLegacy= */ false,
+ /* submittabilityCondition= */ "label:Code-Review=+2");
+ assertSubmitRequirementStatus(
+ changeInfo.submitRequirements,
+ "New-Requirement",
+ Status.SATISFIED,
+ /* isLegacy= */ false,
+ /* submittabilityCondition= */ "-has:unresolved");
+ }
+ }
+
+ @Test
+ @GerritConfig(
+ name = "experiments.enabled",
+ values = {
+ ExperimentFeaturesConstants.GERRIT_BACKEND_REQUEST_FEATURE_ENABLE_SUBMIT_REQUIREMENTS,
+ ExperimentFeaturesConstants
+ .GERRIT_BACKEND_REQUEST_FEATURE_STORE_SUBMIT_REQUIREMENTS_ON_MERGE
+ })
public void submitRequirement_retrievedFromNoteDbForClosedChanges() throws Exception {
configSubmitRequirement(
project,
diff --git a/javatests/com/google/gerrit/acceptance/server/project/SubmitRequirementsEvaluatorIT.java b/javatests/com/google/gerrit/acceptance/server/project/SubmitRequirementsEvaluatorIT.java
index f511683..b76d5cb 100644
--- a/javatests/com/google/gerrit/acceptance/server/project/SubmitRequirementsEvaluatorIT.java
+++ b/javatests/com/google/gerrit/acceptance/server/project/SubmitRequirementsEvaluatorIT.java
@@ -25,6 +25,7 @@
import com.google.gerrit.acceptance.PushOneCommit;
import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
import com.google.gerrit.common.Nullable;
+import com.google.gerrit.entities.Change;
import com.google.gerrit.entities.LabelFunction;
import com.google.gerrit.entities.SubmitRequirement;
import com.google.gerrit.entities.SubmitRequirementExpression;
@@ -32,6 +33,7 @@
import com.google.gerrit.entities.SubmitRequirementExpressionResult.Status;
import com.google.gerrit.entities.SubmitRequirementResult;
import com.google.gerrit.extensions.api.changes.ReviewInput;
+import com.google.gerrit.extensions.common.ChangeInfo;
import com.google.gerrit.extensions.restapi.RestApiException;
import com.google.gerrit.server.project.SubmitRequirementsEvaluator;
import com.google.gerrit.server.query.change.ChangeData;
@@ -215,6 +217,33 @@
.isEqualTo("Unsupported operator invalid_field:invalid_value");
}
+ @Test
+ public void byPureRevert() throws Exception {
+ testRepo.reset("HEAD~1");
+ PushOneCommit.Result pushResult =
+ createChange(testRepo, "refs/heads/master", "Fix a bug", "file.txt", "content", "topic");
+ changeData = pushResult.getChange();
+ changeId = pushResult.getChangeId();
+
+ SubmitRequirement sr =
+ createSubmitRequirement(
+ /* applicabilityExpr= */ "project:" + project.get(),
+ /* submittabilityExpr= */ "is:pure-revert",
+ /* overrideExpr= */ "");
+
+ SubmitRequirementResult result = evaluator.evaluateRequirement(sr, changeData);
+ assertThat(result.status()).isEqualTo(SubmitRequirementResult.Status.UNSATISFIED);
+ approve(changeId);
+ gApi.changes().id(changeId).current().submit();
+
+ ChangeInfo changeInfo = gApi.changes().id(changeId).revert().get();
+ String revertId = Integer.toString(changeInfo._number);
+ ChangeData revertChangeData =
+ changeQueryProvider.get().byLegacyChangeId(Change.Id.parse(revertId)).get(0);
+ result = evaluator.evaluateRequirement(sr, revertChangeData);
+ assertThat(result.status()).isEqualTo(SubmitRequirementResult.Status.SATISFIED);
+ }
+
private void voteLabel(String changeId, String labelName, int score) throws RestApiException {
gApi.changes().id(changeId).current().review(new ReviewInput().label(labelName, score));
}
diff --git a/javatests/com/google/gerrit/server/BUILD b/javatests/com/google/gerrit/server/BUILD
index 0527a91..689698e 100644
--- a/javatests/com/google/gerrit/server/BUILD
+++ b/javatests/com/google/gerrit/server/BUILD
@@ -54,6 +54,7 @@
"//java/com/google/gerrit/proto/testing",
"//java/com/google/gerrit/server",
"//java/com/google/gerrit/server/account/externalids/testing",
+ "//java/com/google/gerrit/server/cache/mem",
"//java/com/google/gerrit/server/cache/serialize",
"//java/com/google/gerrit/server/cache/testing",
"//java/com/google/gerrit/server/cancellation",
diff --git a/javatests/com/google/gerrit/server/mail/send/MailSoySauceProviderTest.java b/javatests/com/google/gerrit/server/mail/send/MailSoySauceLoaderTest.java
similarity index 89%
rename from javatests/com/google/gerrit/server/mail/send/MailSoySauceProviderTest.java
rename to javatests/com/google/gerrit/server/mail/send/MailSoySauceLoaderTest.java
index 2ec5e4d..fbeabe1 100644
--- a/javatests/com/google/gerrit/server/mail/send/MailSoySauceProviderTest.java
+++ b/javatests/com/google/gerrit/server/mail/send/MailSoySauceLoaderTest.java
@@ -25,7 +25,7 @@
import org.junit.Before;
import org.junit.Test;
-public class MailSoySauceProviderTest {
+public class MailSoySauceLoaderTest {
private SitePaths sitePaths;
private DynamicSet<MailSoyTemplateProvider> set;
@@ -38,11 +38,11 @@
@Test
public void soyCompilation() {
- MailSoySauceProvider provider =
- new MailSoySauceProvider(
+ MailSoySauceLoader loader =
+ new MailSoySauceLoader(
sitePaths,
new SoyAstCache(),
new PluginSetContext<>(set, PluginMetrics.DISABLED_INSTANCE));
- assertThat(provider.get()).isNotNull(); // should not throw
+ assertThat(loader.load()).isNotNull(); // should not throw
}
}
diff --git a/javatests/com/google/gerrit/server/mail/send/MailSoySauceModuleTest.java b/javatests/com/google/gerrit/server/mail/send/MailSoySauceModuleTest.java
new file mode 100644
index 0000000..bb443f8
--- /dev/null
+++ b/javatests/com/google/gerrit/server/mail/send/MailSoySauceModuleTest.java
@@ -0,0 +1,60 @@
+package com.google.gerrit.server.mail.send;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.util.concurrent.MoreExecutors.newDirectExecutorService;
+
+import com.google.common.cache.LoadingCache;
+import com.google.common.util.concurrent.ListeningExecutorService;
+import com.google.gerrit.metrics.DisabledMetricMaker;
+import com.google.gerrit.metrics.MetricMaker;
+import com.google.gerrit.server.CacheRefreshExecutor;
+import com.google.gerrit.server.cache.mem.DefaultMemoryCacheModule;
+import com.google.gerrit.server.config.GerritServerConfig;
+import com.google.gerrit.server.config.SitePaths;
+import com.google.inject.AbstractModule;
+import com.google.inject.Guice;
+import com.google.inject.Injector;
+import com.google.inject.Key;
+import com.google.inject.TypeLiteral;
+import com.google.inject.name.Names;
+import com.google.template.soy.jbcsrc.api.SoySauce;
+import java.nio.file.Paths;
+import javax.inject.Provider;
+import org.eclipse.jgit.lib.Config;
+import org.junit.Test;
+
+public class MailSoySauceModuleTest {
+ @Test
+ public void soySauceProviderReturnsCachedValue() throws Exception {
+ SitePaths sitePaths = new SitePaths(Paths.get("."));
+ Injector injector =
+ Guice.createInjector(
+ new MailSoySauceModule(),
+ new AbstractModule() {
+ @Override
+ protected void configure() {
+ super.configure();
+ bind(ListeningExecutorService.class)
+ .annotatedWith(CacheRefreshExecutor.class)
+ .toInstance(newDirectExecutorService());
+ bind(SitePaths.class).toInstance(sitePaths);
+ bind(Config.class).annotatedWith(GerritServerConfig.class).toInstance(new Config());
+ bind(MetricMaker.class).to(DisabledMetricMaker.class);
+ install(new DefaultMemoryCacheModule());
+ }
+ });
+ Provider<SoySauce> soySauceProvider =
+ injector.getProvider(Key.get(SoySauce.class, MailTemplates.class));
+ LoadingCache<String, SoySauce> cache =
+ injector.getInstance(
+ Key.get(
+ new TypeLiteral<LoadingCache<String, SoySauce>>() {},
+ Names.named(MailSoySauceModule.CACHE_NAME)));
+ assertThat(cache.stats().loadCount()).isEqualTo(0);
+ // Theoretically, this can be flaky, if the delay before the second get takes several seconds.
+ // We assume that tests is fast enough.
+ assertThat(soySauceProvider.get()).isNotNull();
+ assertThat(soySauceProvider.get()).isNotNull();
+ assertThat(cache.stats().loadCount()).isEqualTo(1);
+ }
+}
diff --git a/javatests/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java b/javatests/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java
index 3b671aa..5253a5b 100644
--- a/javatests/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java
+++ b/javatests/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java
@@ -26,6 +26,7 @@
import static com.google.gerrit.server.project.testing.TestLabels.label;
import static com.google.gerrit.server.project.testing.TestLabels.value;
import static com.google.gerrit.testing.GerritJUnit.assertThrows;
+import static java.nio.charset.StandardCharsets.UTF_8;
import static java.util.concurrent.TimeUnit.HOURS;
import static java.util.concurrent.TimeUnit.MILLISECONDS;
import static java.util.concurrent.TimeUnit.MINUTES;
@@ -40,6 +41,9 @@
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
+import com.google.common.collect.Multimap;
+import com.google.common.collect.Multimaps;
import com.google.common.collect.Streams;
import com.google.common.truth.ThrowableSubject;
import com.google.gerrit.acceptance.ExtensionRegistry;
@@ -48,6 +52,7 @@
import com.google.gerrit.acceptance.config.GerritConfig;
import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
import com.google.gerrit.common.Nullable;
+import com.google.gerrit.common.RawInputUtil;
import com.google.gerrit.entities.Account;
import com.google.gerrit.entities.AccountGroup;
import com.google.gerrit.entities.BranchNameKey;
@@ -141,7 +146,6 @@
import java.util.Arrays;
import java.util.Collection;
import java.util.Iterator;
-import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;
@@ -1038,6 +1042,7 @@
ChangeInserter ins3 = newChange(repo);
ChangeInserter ins4 = newChange(repo);
ChangeInserter ins5 = newChange(repo);
+ ChangeInserter ins6 = newChange(repo);
Change reviewMinus2Change = insert(repo, ins);
gApi.changes().id(reviewMinus2Change.getId().get()).current().review(ReviewInput.reject());
@@ -1050,7 +1055,13 @@
Change reviewPlus1Change = insert(repo, ins4);
gApi.changes().id(reviewPlus1Change.getId().get()).current().review(ReviewInput.recommend());
- Change reviewPlus2Change = insert(repo, ins5);
+ Change reviewTwoPlus1Change = insert(repo, ins5);
+ gApi.changes().id(reviewTwoPlus1Change.getId().get()).current().review(ReviewInput.recommend());
+ requestContext.setContext(newRequestContext(createAccount("user1")));
+ gApi.changes().id(reviewTwoPlus1Change.getId().get()).current().review(ReviewInput.recommend());
+ requestContext.setContext(newRequestContext(userId));
+
+ Change reviewPlus2Change = insert(repo, ins6);
gApi.changes().id(reviewPlus2Change.getId().get()).current().review(ReviewInput.approve());
Map<String, Short> m =
@@ -1061,8 +1072,10 @@
assertThat(m).hasSize(1);
assertThat(m).containsEntry("Code-Review", Short.valueOf((short) 1));
- Map<Integer, Change> changes = new LinkedHashMap<>(5);
+ Multimap<Integer, Change> changes =
+ Multimaps.newListMultimap(Maps.newLinkedHashMap(), () -> Lists.newArrayList());
changes.put(2, reviewPlus2Change);
+ changes.put(1, reviewTwoPlus1Change);
changes.put(1, reviewPlus1Change);
changes.put(0, noLabelChange);
changes.put(-1, reviewMinus1Change);
@@ -1074,9 +1087,9 @@
assertQuery("label:Code-Review=-1", reviewMinus1Change);
assertQuery("label:Code-Review-1", reviewMinus1Change);
assertQuery("label:Code-Review=0", noLabelChange);
- assertQuery("label:Code-Review=+1", reviewPlus1Change);
- assertQuery("label:Code-Review=1", reviewPlus1Change);
- assertQuery("label:Code-Review+1", reviewPlus1Change);
+ assertQuery("label:Code-Review=+1", reviewTwoPlus1Change, reviewPlus1Change);
+ assertQuery("label:Code-Review=1", reviewTwoPlus1Change, reviewPlus1Change);
+ assertQuery("label:Code-Review+1", reviewTwoPlus1Change, reviewPlus1Change);
assertQuery("label:Code-Review=+2", reviewPlus2Change);
assertQuery("label:Code-Review=2", reviewPlus2Change);
assertQuery("label:Code-Review+2", reviewPlus2Change);
@@ -1084,6 +1097,7 @@
assertQuery(
"label:Code-Review=ANY",
reviewPlus2Change,
+ reviewTwoPlus1Change,
reviewPlus1Change,
reviewMinus1Change,
reviewMinus2Change);
@@ -1112,14 +1126,70 @@
assertQuery("label:Code-Review<-2");
assertQuery("label:Code-Review=+1,anotheruser");
- assertQuery("label:Code-Review=+1,user", reviewPlus1Change);
- assertQuery("label:Code-Review=+1,user=user", reviewPlus1Change);
- assertQuery("label:Code-Review=+1,Administrators", reviewPlus1Change);
- assertQuery("label:Code-Review=+1,group=Administrators", reviewPlus1Change);
- assertQuery("label:Code-Review=+1,user=owner", reviewPlus1Change);
- assertQuery("label:Code-Review=+1,owner", reviewPlus1Change);
+ assertQuery("label:Code-Review=+1,user", reviewTwoPlus1Change, reviewPlus1Change);
+ assertQuery("label:Code-Review=+1,user=user", reviewTwoPlus1Change, reviewPlus1Change);
+ assertQuery("label:Code-Review=+1,Administrators", reviewTwoPlus1Change, reviewPlus1Change);
+ assertQuery(
+ "label:Code-Review=+1,group=Administrators", reviewTwoPlus1Change, reviewPlus1Change);
+ assertQuery("label:Code-Review=+1,user=owner", reviewTwoPlus1Change, reviewPlus1Change);
+ assertQuery("label:Code-Review=+1,owner", reviewTwoPlus1Change, reviewPlus1Change);
assertQuery("label:Code-Review=+2,owner", reviewPlus2Change);
assertQuery("label:Code-Review=-2,owner", reviewMinus2Change);
+
+ // count=0 is not allowed
+ Exception thrown =
+ assertThrows(BadRequestException.class, () -> assertQuery("label:Code-Review=+2,count=0"));
+ assertThat(thrown).hasMessageThat().isEqualTo("Argument count=0 is not allowed.");
+ assertQuery("label:Code-Review=1,count=1", reviewPlus1Change);
+ assertQuery("label:Code-Review=1,count=2", reviewTwoPlus1Change);
+ assertQuery("label:Code-Review=1,count>=2", reviewTwoPlus1Change);
+ assertQuery("label:Code-Review=1,count>1", reviewTwoPlus1Change);
+ assertQuery("label:Code-Review=1,count>=1", reviewTwoPlus1Change, reviewPlus1Change);
+ assertQuery("label:Code-Review=1,count=3");
+ thrown =
+ assertThrows(BadRequestException.class, () -> assertQuery("label:Code-Review=1,count=7"));
+ assertThat(thrown)
+ .hasMessageThat()
+ .isEqualTo("count=7 is not allowed. Maximum allowed value for count is 5.");
+
+ // Less than operator does not match with changes having count=0 for a specific vote value (i.e.
+ // no votes for that specific value). We do that deliberately since the computation of count=0
+ // for label values is expensive when the change is re-indexed. This is because the operation
+ // requires generating all formats for all {label-type, vote}=0 values that are non-voted for
+ // the change and storing them with the 'count=0' format.
+ assertQuery("label:Code-Review=1,count<5", reviewTwoPlus1Change, reviewPlus1Change);
+ assertQuery("label:Code-Review=1,count<=5", reviewTwoPlus1Change, reviewPlus1Change);
+ assertQuery(
+ "label:Code-Review=1,count<=1", // reviewTwoPlus1Change is not matched since its count=2
+ reviewPlus1Change);
+ assertQuery(
+ "label:Code-Review=1,count<5 label:Code-Review=1,count>=1",
+ reviewTwoPlus1Change,
+ reviewPlus1Change);
+ assertQuery(
+ "label:Code-Review=1,count<=5 label:Code-Review=1,count>=1",
+ reviewTwoPlus1Change,
+ reviewPlus1Change);
+ assertQuery("label:Code-Review=1,count<=1 label:Code-Review=1,count>=1", reviewPlus1Change);
+
+ assertQuery("label:Code-Review=MAX,count=1", reviewPlus2Change);
+ assertQuery("label:Code-Review=MAX,count=2");
+ assertQuery("label:Code-Review=MIN,count=1", reviewMinus2Change);
+ assertQuery("label:Code-Review=MIN,count>1");
+ assertQuery("label:Code-Review=MAX,count<2", reviewPlus2Change);
+ assertQuery("label:Code-Review=MIN,count<1");
+ assertQuery("label:Code-Review=MAX,count<2 label:Code-Review=MAX,count>=1", reviewPlus2Change);
+ assertQuery("label:Code-Review=MIN,count<1 label:Code-Review=MIN,count>=1");
+ assertQuery("label:Code-Review>=+1,count=2", reviewTwoPlus1Change);
+
+ // "count" and "user" args cannot be used simultaneously.
+ assertThrows(
+ BadRequestException.class,
+ () -> assertQuery("label:Code-Review=+1,user=non_uploader,count=2"));
+
+ // "count" and "group" args cannot be used simultaneously.
+ assertThrows(
+ BadRequestException.class, () -> assertQuery("label:Code-Review=+1,group=gerrit,count=2"));
}
@Test
@@ -1224,16 +1294,15 @@
assertQuery("label:Code-Review=+1,non_uploader", reviewPlus1Change);
}
- private Change[] codeReviewInRange(Map<Integer, Change> changes, int start, int end) {
- int size = 0;
- Change[] range = new Change[end - start + 1];
- for (int i : changes.keySet()) {
+ private Change[] codeReviewInRange(Multimap<Integer, Change> changes, int start, int end) {
+ List<Change> range = new ArrayList<>();
+ for (Map.Entry<Integer, Change> entry : changes.entries()) {
+ int i = entry.getKey();
if (i >= start && i <= end) {
- range[size] = changes.get(i);
- size++;
+ range.add(entry.getValue());
}
}
- return range;
+ return range.toArray(new Change[0]);
}
private String createGroup(String name, String owner) throws Exception {
@@ -3891,6 +3960,33 @@
}
@Test
+ public void isPureRevert() throws Exception {
+ assume().that(getSchema().hasField(ChangeField.IS_PURE_REVERT)).isTrue();
+ TestRepository<Repo> repo = createProject("repo");
+ // Create two commits and revert second commit (initial commit can't be reverted)
+ Change initial = insert(repo, newChange(repo));
+ gApi.changes().id(initial.getChangeId()).current().review(ReviewInput.approve());
+ gApi.changes().id(initial.getChangeId()).current().submit();
+
+ ChangeInfo changeToRevert =
+ gApi.changes().create(new ChangeInput("repo", "master", "commit to revert")).get();
+ gApi.changes().id(changeToRevert.id).current().review(ReviewInput.approve());
+ gApi.changes().id(changeToRevert.id).current().submit();
+
+ ChangeInfo changeThatReverts = gApi.changes().id(changeToRevert.id).revert().get();
+ Change.Id changeThatRevertsId = Change.id(changeThatReverts._number);
+ assertQueryByIds("is:pure-revert", changeThatRevertsId);
+
+ // Update the change that reverts such that it's not a pure revert
+ gApi.changes()
+ .id(changeThatReverts.id)
+ .edit()
+ .modifyFile("some-file.txt", RawInputUtil.create("newcontent".getBytes(UTF_8)));
+ gApi.changes().id(changeThatReverts.id).edit().publish();
+ assertQueryByIds("is:pure-revert");
+ }
+
+ @Test
public void selfFailsForAnonymousUser() throws Exception {
for (String query : ImmutableList.of("assignee:self", "has:star", "is:starred", "star:star")) {
assertQuery(query);
diff --git a/plugins/package.json b/plugins/package.json
index 4e3c376..e5d245c 100644
--- a/plugins/package.json
+++ b/plugins/package.json
@@ -6,7 +6,7 @@
"@polymer/decorators": "^3.0.0",
"@polymer/polymer": "^3.4.1",
"@gerritcodereview/typescript-api": "3.4.4",
- "lit": "2.0.0-rc.3"
+ "lit": "^2.0.2"
},
"license": "Apache-2.0",
"private": true
diff --git a/plugins/yarn.lock b/plugins/yarn.lock
index 3ff1cc4..4cbe489 100644
--- a/plugins/yarn.lock
+++ b/plugins/yarn.lock
@@ -7,10 +7,10 @@
resolved "https://registry.yarnpkg.com/@gerritcodereview/typescript-api/-/typescript-api-3.4.4.tgz#9f09687038088dd7edd3b4e30d249502eb21bfbc"
integrity sha512-MAiQwntcQ59b92yYDsVIXj3oBbAB4C7HELkLFFbYs4ZjzC43XqqtR9VF0dh5OUC8wzFZttgUiOmGehk9edpPuw==
-"@lit/reactive-element@^1.0.0-rc.2":
- version "1.0.0-rc.2"
- resolved "https://registry.yarnpkg.com/@lit/reactive-element/-/reactive-element-1.0.0-rc.2.tgz#f24dba16ea571a08dca70f1783bd2ca5ec8de3ee"
- integrity sha512-cujeIl5Ei8FC7UHf4/4Q3bRJOtdTe1vpJV/JEBYCggedmQ+2P8A2oz7eE+Vxi6OJ4nc0X+KZxXnBoH4QrEbmEQ==
+"@lit/reactive-element@^1.0.0":
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/@lit/reactive-element/-/reactive-element-1.0.1.tgz#853cacd4d78d79059f33f66f8e7b0e5c34bee294"
+ integrity sha512-nSD5AA2AZkKuXuvGs8IK7K5ZczLAogfDd26zT9l6S7WzvqALdVWcW5vMUiTnZyj5SPcNwNNANj0koeV1ieqTFQ==
"@polymer/decorators@^3.0.0":
version "3.0.0"
@@ -26,43 +26,36 @@
dependencies:
"@webcomponents/shadycss" "^1.9.1"
-"@types/trusted-types@^1.0.1":
- version "1.0.6"
- resolved "https://registry.yarnpkg.com/@types/trusted-types/-/trusted-types-1.0.6.tgz#569b8a08121d3203398290d602d84d73c8dcf5da"
- integrity sha512-230RC8sFeHoT6sSUlRO6a8cAnclO06eeiq1QDfiv2FGCLWFvvERWgwIQD4FWqD9A69BN7Lzee4OXwoMVnnsWDw==
+"@types/trusted-types@^2.0.2":
+ version "2.0.2"
+ resolved "https://registry.yarnpkg.com/@types/trusted-types/-/trusted-types-2.0.2.tgz#fc25ad9943bcac11cceb8168db4f275e0e72e756"
+ integrity sha512-F5DIZ36YVLE+PN+Zwws4kJogq47hNgX3Nx6WyDJ3kcplxyke3XIzB8uK5n/Lpm1HBsbGzd6nmGehL8cPekP+Tg==
"@webcomponents/shadycss@^1.9.1":
version "1.11.0"
resolved "https://registry.yarnpkg.com/@webcomponents/shadycss/-/shadycss-1.11.0.tgz#73e289996c002d8be694cd3be0e83c46ad25e7e0"
integrity sha512-L5O/+UPum8erOleNjKq6k58GVl3fNsEQdSOyh0EUhNmi7tHUyRuCJy1uqJiWydWcLARE5IPsMoPYMZmUGrz1JA==
-lit-element@^3.0.0-rc.2:
- version "3.0.0-rc.2"
- resolved "https://registry.yarnpkg.com/lit-element/-/lit-element-3.0.0-rc.2.tgz#883d0b6fd7b846226d360699d1b713da5fc7e1b7"
- integrity sha512-2Z7DabJ3b5K+p5073vFjMODoaWqy5PIaI4y6ADKm+fCGc8OnX9fU9dMoUEBZjFpd/bEFR9PBp050tUtBnT9XTQ==
+lit-element@^3.0.0:
+ version "3.0.1"
+ resolved "https://registry.yarnpkg.com/lit-element/-/lit-element-3.0.1.tgz#3c545af17d8a46268bc1dd5623a47486e6ff76f4"
+ integrity sha512-vs9uybH9ORyK49CFjoNGN85HM9h5bmisU4TQ63phe/+GYlwvY/3SIFYKdjV6xNvzz8v2MnVC+9+QOkPqh+Q3Ew==
dependencies:
- "@lit/reactive-element" "^1.0.0-rc.2"
- lit-html "^2.0.0-rc.3"
+ "@lit/reactive-element" "^1.0.0"
+ lit-html "^2.0.0"
-lit-html@^2.0.0-rc.3:
- version "2.0.0-rc.3"
- resolved "https://registry.yarnpkg.com/lit-html/-/lit-html-2.0.0-rc.3.tgz#1c216e548630e18d3093d97f4e29563abce659af"
- integrity sha512-Y6P8LlAyQuqvzq6l/Nc4z5/P5M/rVLYKQIRxcNwSuGajK0g4kbcBFQqZmgvqKG+ak+dHZjfm2HUw9TF5N/pkCw==
+lit-html@^2.0.0:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/lit-html/-/lit-html-2.0.1.tgz#63241015efa07bc9259b6f96f04abd052d2a1f95"
+ integrity sha512-KF5znvFdXbxTYM/GjpdOOnMsjgRcFGusTnB54ixnCTya5zUR0XqrDRj29ybuLS+jLXv1jji6Y8+g4W7WP8uL4w==
dependencies:
- "@types/trusted-types" "^1.0.1"
+ "@types/trusted-types" "^2.0.2"
-lit-html@^2.0.0-rc.4:
- version "2.0.0-rc.4"
- resolved "https://registry.yarnpkg.com/lit-html/-/lit-html-2.0.0-rc.4.tgz#1015fa8f1f7c8c5b79999ed0bc11c3b79ff1aab5"
- integrity sha512-WSLGu3vxq7y8q/oOd9I3zxyBELNLLiDk6gAYoKK4PGctI5fbh6lhnO/jVBdy0PV/vTc+cLJCA/occzx3YoNPeg==
+lit@^2.0.2:
+ version "2.0.2"
+ resolved "https://registry.yarnpkg.com/lit/-/lit-2.0.2.tgz#5e6f422924e0732258629fb379556b6d23f7179c"
+ integrity sha512-hKA/1YaSB+P+DvKWuR2q1Xzy/iayhNrJ3aveD0OQ9CKn6wUjsdnF/7LavDOJsKP/K5jzW/kXsuduPgRvTFrFJw==
dependencies:
- "@types/trusted-types" "^1.0.1"
-
-lit@2.0.0-rc.3:
- version "2.0.0-rc.3"
- resolved "https://registry.yarnpkg.com/lit/-/lit-2.0.0-rc.3.tgz#8b6a85268aba287c11125dfe57e88e0bc09beaff"
- integrity sha512-UZDLWuspl7saA+WvS0e+TE3NdGGE05hOIwUPTWiibs34c5QupcEzpjB/aElt79V9bELQVNbUUwa0Ow7D1Wuszw==
- dependencies:
- "@lit/reactive-element" "^1.0.0-rc.2"
- lit-element "^3.0.0-rc.2"
- lit-html "^2.0.0-rc.4"
+ "@lit/reactive-element" "^1.0.0"
+ lit-element "^3.0.0"
+ lit-html "^2.0.0"
diff --git a/polygerrit-ui/app/BUILD b/polygerrit-ui/app/BUILD
index 3a647d4..980abb4 100644
--- a/polygerrit-ui/app/BUILD
+++ b/polygerrit-ui/app/BUILD
@@ -97,7 +97,6 @@
"elements/admin/gr-admin-view/gr-admin-view_html.ts",
"elements/admin/gr-create-repo-dialog/gr-create-repo-dialog_html.ts",
"elements/admin/gr-group-members/gr-group-members_html.ts",
- "elements/admin/gr-group/gr-group_html.ts",
"elements/admin/gr-permission/gr-permission_html.ts",
"elements/admin/gr-plugin-list/gr-plugin-list_html.ts",
"elements/admin/gr-repo-access/gr-repo-access_html.ts",
@@ -125,7 +124,6 @@
"elements/diff/gr-diff-preferences-dialog/gr-diff-preferences-dialog_html.ts",
"elements/diff/gr-diff-view/gr-diff-view_html.ts",
"elements/diff/gr-diff/gr-diff_html.ts",
- "elements/diff/gr-patch-range-select/gr-patch-range-select_html.ts",
"elements/gr-app-element_html.ts",
"elements/settings/gr-watched-projects-editor/gr-watched-projects-editor_html.ts",
"elements/shared/gr-account-list/gr-account-list_html.ts",
diff --git a/polygerrit-ui/app/api/rest-api.ts b/polygerrit-ui/app/api/rest-api.ts
index 39b40b6..e2b1502 100644
--- a/polygerrit-ui/app/api/rest-api.ts
+++ b/polygerrit-ui/app/api/rest-api.ts
@@ -1065,7 +1065,7 @@
url: string;
/** URL to the icon of the link. */
image_url?: string;
- /* The links target. */
+ /* Value of the "target" attribute for anchor elements. */
target?: string;
}
diff --git a/polygerrit-ui/app/constants/reporting.ts b/polygerrit-ui/app/constants/reporting.ts
index a10bdda..0029f5c 100644
--- a/polygerrit-ui/app/constants/reporting.ts
+++ b/polygerrit-ui/app/constants/reporting.ts
@@ -98,4 +98,6 @@
TOGGLE_SHOW_ALL_BUTTON = 'toggle show all button',
SHOW_TAB = 'show-tab',
ATTENTION_SET_CHIP = 'attention-set-chip',
+ SAVE_COMMENT = 'save-comment',
+ COMMENT_SAVED = 'comment-saved',
}
diff --git a/polygerrit-ui/app/elements/admin/gr-admin-view/gr-admin-view_test.js b/polygerrit-ui/app/elements/admin/gr-admin-view/gr-admin-view_test.js
index cf0fdd4..6bd1ac5 100644
--- a/polygerrit-ui/app/elements/admin/gr-admin-view/gr-admin-view_test.js
+++ b/polygerrit-ui/app/elements/admin/gr-admin-view/gr-admin-view_test.js
@@ -505,7 +505,7 @@
suite('groups', () => {
let getGroupConfigStub;
setup(() => {
- stub('gr-group', '_loadGroup').callsFake(() => Promise.resolve({}));
+ stub('gr-group', 'loadGroup').callsFake(() => Promise.resolve({}));
stub('gr-group-members', '_loadGroupDetails').callsFake(() => {});
getGroupConfigStub = stubRestApi('getGroupConfig');
diff --git a/polygerrit-ui/app/elements/admin/gr-create-repo-dialog/gr-create-repo-dialog.ts b/polygerrit-ui/app/elements/admin/gr-create-repo-dialog/gr-create-repo-dialog.ts
index a493747..63f6601 100644
--- a/polygerrit-ui/app/elements/admin/gr-create-repo-dialog/gr-create-repo-dialog.ts
+++ b/polygerrit-ui/app/elements/admin/gr-create-repo-dialog/gr-create-repo-dialog.ts
@@ -20,6 +20,8 @@
import '../../shared/gr-autocomplete/gr-autocomplete';
import '../../shared/gr-button/gr-button';
import '../../shared/gr-select/gr-select';
+import {GrAutocomplete} from '../../shared/gr-autocomplete/gr-autocomplete';
+import {GrSelect} from '../../shared/gr-select/gr-select';
import {PolymerElement} from '@polymer/polymer/polymer-element';
import {htmlTemplate} from './gr-create-repo-dialog_html';
import {encodeURL, getBaseUrl} from '../../../utils/url-util';
@@ -40,6 +42,15 @@
}
}
+export interface GrCreateRepoDialog {
+ $: {
+ initialCommit: GrSelect;
+ parentRepo: GrSelect;
+ repoNameInput: HTMLInputElement;
+ rightsInheritFromInput: GrAutocomplete;
+ };
+}
+
@customElement('gr-create-repo-dialog')
export class GrCreateRepoDialog extends PolymerElement {
static get template() {
diff --git a/polygerrit-ui/app/elements/admin/gr-create-repo-dialog/gr-create-repo-dialog_html.ts b/polygerrit-ui/app/elements/admin/gr-create-repo-dialog/gr-create-repo-dialog_html.ts
index f529ac6..d0a6b7f 100644
--- a/polygerrit-ui/app/elements/admin/gr-create-repo-dialog/gr-create-repo-dialog_html.ts
+++ b/polygerrit-ui/app/elements/admin/gr-create-repo-dialog/gr-create-repo-dialog_html.ts
@@ -36,24 +36,14 @@
<div id="form">
<section>
<span class="title">Repository name</span>
- <iron-input autocomplete="on" bind-value="{{_repoConfig.name}}">
- <input
- is="iron-input"
- id="repoNameInput"
- autocomplete="on"
- bind-value="{{_repoConfig.name}}"
- />
+ <iron-input bind-value="{{_repoConfig.name}}">
+ <input id="repoNameInput" autocomplete="on" />
</iron-input>
</section>
<section>
<span class="title">Default Branch</span>
- <iron-input autocomplete="off" bind-value="{{_defaultBranch}}">
- <input
- is="iron-input"
- id="defaultBranchNameInput"
- autocomplete="off"
- bind-value="{{_defaultBranch}}"
- />
+ <iron-input bind-value="{{_defaultBranch}}">
+ <input id="defaultBranchNameInput" autocomplete="off" />
</iron-input>
</section>
<section>
diff --git a/polygerrit-ui/app/elements/admin/gr-create-repo-dialog/gr-create-repo-dialog_test.js b/polygerrit-ui/app/elements/admin/gr-create-repo-dialog/gr-create-repo-dialog_test.js
deleted file mode 100644
index f1babee..0000000
--- a/polygerrit-ui/app/elements/admin/gr-create-repo-dialog/gr-create-repo-dialog_test.js
+++ /dev/null
@@ -1,80 +0,0 @@
-/**
- * @license
- * Copyright (C) 2017 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-import '../../../test/common-test-setup-karma.js';
-import './gr-create-repo-dialog.js';
-import {stubRestApi} from '../../../test/test-utils.js';
-
-const basicFixture = fixtureFromElement('gr-create-repo-dialog');
-
-suite('gr-create-repo-dialog tests', () => {
- let element;
-
- setup(() => {
- element = basicFixture.instantiate();
- });
-
- test('default values are populated', () => {
- assert.isTrue(element.$.initialCommit.bindValue);
- assert.isFalse(element.$.parentRepo.bindValue);
- });
-
- test('repo created', async () => {
- const configInputObj = {
- name: 'test-repo',
- create_empty_commit: true,
- parent: 'All-Project',
- permissions_only: false,
- };
-
- const saveStub = stubRestApi('createRepo').returns(Promise.resolve({}));
-
- assert.isFalse(element.hasNewRepoName);
-
- element._repoConfig = {
- name: 'test-repo',
- create_empty_commit: true,
- parent: 'All-Project',
- permissions_only: false,
- };
-
- element._repoOwner = 'test';
- element._repoOwnerId = 'testId';
- element._defaultBranch = 'main';
-
- element.$.repoNameInput.bindValue = configInputObj.name;
- element.$.rightsInheritFromInput.bindValue = configInputObj.parent;
- element.$.initialCommit.bindValue =
- configInputObj.create_empty_commit;
- element.$.parentRepo.bindValue =
- configInputObj.permissions_only;
-
- assert.isTrue(element.hasNewRepoName);
-
- assert.deepEqual(element._repoConfig, configInputObj);
-
- await element.handleCreateRepo();
- assert.isTrue(saveStub.lastCall.calledWithExactly(
- {
- ...configInputObj,
- owners: ['testId'],
- branches: ['main'],
- }
- ));
- });
-});
-
diff --git a/polygerrit-ui/app/elements/admin/gr-create-repo-dialog/gr-create-repo-dialog_test.ts b/polygerrit-ui/app/elements/admin/gr-create-repo-dialog/gr-create-repo-dialog_test.ts
new file mode 100644
index 0000000..6485bae
--- /dev/null
+++ b/polygerrit-ui/app/elements/admin/gr-create-repo-dialog/gr-create-repo-dialog_test.ts
@@ -0,0 +1,81 @@
+/**
+ * @license
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import '../../../test/common-test-setup-karma';
+import './gr-create-repo-dialog';
+import {GrCreateRepoDialog} from './gr-create-repo-dialog';
+import {stubRestApi} from '../../../test/test-utils';
+import {BranchName, GroupId, RepoName} from '../../../types/common';
+
+const basicFixture = fixtureFromElement('gr-create-repo-dialog');
+
+suite('gr-create-repo-dialog tests', () => {
+ let element: GrCreateRepoDialog;
+
+ setup(() => {
+ element = basicFixture.instantiate();
+ });
+
+ test('default values are populated', () => {
+ assert.isTrue(element.$.initialCommit.bindValue);
+ assert.isFalse(element.$.parentRepo.bindValue);
+ });
+
+ test('repo created', async () => {
+ const configInputObj = {
+ name: 'test-repo' as RepoName,
+ create_empty_commit: true,
+ parent: 'All-Project' as RepoName,
+ permissions_only: false,
+ };
+
+ const saveStub = stubRestApi('createRepo').returns(
+ Promise.resolve(new Response())
+ );
+
+ assert.isFalse(element.hasNewRepoName);
+
+ element._repoConfig = {
+ name: 'test-repo' as RepoName,
+ create_empty_commit: true,
+ parent: 'All-Project' as RepoName,
+ permissions_only: false,
+ };
+
+ element._repoOwner = 'test';
+ element._repoOwnerId = 'testId' as GroupId;
+ element._defaultBranch = 'main' as BranchName;
+
+ element.$.repoNameInput.value = configInputObj.name;
+ element.$.rightsInheritFromInput.value = configInputObj.parent;
+ element.$.initialCommit.bindValue = configInputObj.create_empty_commit;
+ element.$.parentRepo.bindValue = configInputObj.permissions_only;
+
+ assert.isTrue(element.hasNewRepoName);
+
+ assert.deepEqual(element._repoConfig, configInputObj);
+
+ await element.handleCreateRepo();
+ assert.isTrue(
+ saveStub.lastCall.calledWithExactly({
+ ...configInputObj,
+ owners: ['testId' as GroupId],
+ branches: ['main' as BranchName],
+ })
+ );
+ });
+});
diff --git a/polygerrit-ui/app/elements/admin/gr-group/gr-group.ts b/polygerrit-ui/app/elements/admin/gr-group/gr-group.ts
index 596fe5b..d7ffbaf 100644
--- a/polygerrit-ui/app/elements/admin/gr-group/gr-group.ts
+++ b/polygerrit-ui/app/elements/admin/gr-group/gr-group.ts
@@ -15,29 +15,27 @@
* limitations under the License.
*/
-import '@polymer/iron-autogrow-textarea/iron-autogrow-textarea';
-import '../../../styles/gr-font-styles';
-import '../../../styles/gr-form-styles';
-import '../../../styles/gr-subpage-styles';
-import '../../../styles/shared-styles';
import '../../shared/gr-autocomplete/gr-autocomplete';
+import '../../shared/gr-button/gr-button';
import '../../shared/gr-copy-clipboard/gr-copy-clipboard';
import '../../shared/gr-select/gr-select';
-import {PolymerElement} from '@polymer/polymer/polymer-element';
-import {htmlTemplate} from './gr-group_html';
-import {customElement, property, observe} from '@polymer/decorators';
+import '../../shared/gr-textarea/gr-textarea';
import {
AutocompleteSuggestion,
AutocompleteQuery,
} from '../../shared/gr-autocomplete/gr-autocomplete';
import {GroupId, GroupInfo, GroupName} from '../../../types/common';
-import {
- fireEvent,
- firePageError,
- fireTitleChange,
-} from '../../../utils/event-util';
+import {firePageError, fireTitleChange} from '../../../utils/event-util';
import {appContext} from '../../../services/app-context';
import {ErrorCallback} from '../../../api/rest';
+import {convertToString} from '../../../utils/string-util';
+import {BindValueChangeEvent} from '../../../types/events';
+import {fontStyles} from '../../../styles/gr-font-styles';
+import {formStyles} from '../../../styles/gr-form-styles';
+import {sharedStyles} from '../../../styles/shared-styles';
+import {subpageStyles} from '../../../styles/gr-subpage-styles';
+import {LitElement, css, html} from 'lit';
+import {customElement, property, state} from 'lit/decorators';
const INTERNAL_GROUP_REGEX = /^[\da-f]{40}$/;
@@ -52,90 +50,267 @@
},
};
-export interface GrGroup {
- $: {
- loading: HTMLDivElement;
- };
-}
-
export interface GroupNameChangedDetail {
name: GroupName;
external: boolean;
}
declare global {
+ interface HTMLElementEventMap {
+ 'text-changed': CustomEvent;
+ 'value-changed': CustomEvent;
+ }
interface HTMLElementTagNameMap {
'gr-group': GrGroup;
}
}
@customElement('gr-group')
-export class GrGroup extends PolymerElement {
- static get template() {
- return htmlTemplate;
- }
-
+export class GrGroup extends LitElement {
/**
* Fired when the group name changes.
*
* @event name-changed
*/
+ private readonly query: AutocompleteQuery;
+
@property({type: String})
groupId?: GroupId;
- @property({type: Boolean})
- _rename = false;
+ @state() private originalOwnerName?: string;
- @property({type: Boolean})
- _groupIsInternal = false;
+ @state() private originalDescriptionName?: string;
- @property({type: Boolean})
- _description = false;
+ @state() private originalOptionsVisibleToAll?: boolean;
- @property({type: Boolean})
- _owner = false;
+ @state() private submitTypes = Object.values(OPTIONS);
- @property({type: Boolean})
- _options = false;
+ /* private but used in test */
+ @state() isAdmin = false;
- @property({type: Boolean})
- _loading = true;
+ /* private but used in test */
+ @state() groupOwner = false;
- @property({type: Object})
- _groupConfig?: GroupInfo;
+ /* private but used in test */
+ @state() groupIsInternal = false;
- @property({type: String})
- _groupConfigOwner?: string;
+ /* private but used in test */
+ @state() loading = true;
- @property({type: Object})
- _groupName?: string;
+ /* private but used in test */
+ @state() groupConfig?: GroupInfo;
- @property({type: Boolean})
- _groupOwner = false;
+ /* private but used in test */
+ @state() groupConfigOwner?: string;
- @property({type: Array})
- _submitTypes = Object.values(OPTIONS);
-
- @property({type: Object})
- _query: AutocompleteQuery;
-
- @property({type: Boolean})
- _isAdmin = false;
+ /* private but used in test */
+ @state() originalName?: GroupName;
private readonly restApiService = appContext.restApiService;
constructor() {
super();
- this._query = (input: string) => this._getGroupSuggestions(input);
+ this.query = (input: string) => this.getGroupSuggestions(input);
}
override connectedCallback() {
super.connectedCallback();
- this._loadGroup();
+ this.loadGroup();
}
- _loadGroup() {
+ static override get styles() {
+ return [
+ fontStyles,
+ formStyles,
+ sharedStyles,
+ subpageStyles,
+ css`
+ h3.edited:after {
+ color: var(--deemphasized-text-color);
+ content: ' *';
+ }
+ `,
+ ];
+ }
+
+ override render() {
+ return html`
+ <div class="main gr-form-styles read-only">
+ <div id="loading" class="${this.computeLoadingClass()}">Loading...</div>
+ <div id="loadedContent" class="${this.computeLoadingClass()}">
+ <h1 id="Title" class="heading-1">
+ ${convertToString(this.originalName)}
+ </h1>
+ <h2 id="configurations" class="heading-2">General</h2>
+ <div id="form">
+ <fieldset>
+ ${this.renderGroupUUID()} ${this.renderGroupName()}
+ ${this.renderGroupOwner()} ${this.renderGroupDescription()}
+ ${this.renderGroupOptions()}
+ </fieldset>
+ </div>
+ </div>
+ </div>
+ `;
+ }
+
+ private renderGroupUUID() {
+ return html`
+ <h3 id="groupUUID" class="heading-3">Group UUID</h3>
+ <fieldset>
+ <gr-copy-clipboard
+ id="uuid"
+ .text=${this.getGroupUUID()}
+ ></gr-copy-clipboard>
+ </fieldset>
+ `;
+ }
+
+ private renderGroupName() {
+ const groupNameEdited = this.originalName !== this.groupConfig?.name;
+ return html`
+ <h3
+ id="groupName"
+ class="heading-3 ${this.computeHeaderClass(groupNameEdited)}"
+ >
+ Group Name
+ </h3>
+ <fieldset>
+ <span class="value">
+ <gr-autocomplete
+ id="groupNameInput"
+ .text=${convertToString(this.groupConfig?.name)}
+ ?disabled=${this.computeGroupDisabled()}
+ @text-changed=${this.handleNameTextChanged}
+ ></gr-autocomplete>
+ </span>
+ <span class="value" ?disabled=${this.computeGroupDisabled()}>
+ <gr-button
+ id="inputUpdateNameBtn"
+ ?disabled=${!groupNameEdited}
+ @click=${this.handleSaveName}
+ >
+ Rename Group</gr-button
+ >
+ </span>
+ </fieldset>
+ `;
+ }
+
+ private renderGroupOwner() {
+ const groupOwnerNameEdited =
+ this.originalOwnerName !== this.groupConfig?.owner;
+ return html`
+ <h3
+ id="groupOwner"
+ class="heading-3 ${this.computeHeaderClass(groupOwnerNameEdited)}"
+ >
+ Owners
+ </h3>
+ <fieldset>
+ <span class="value">
+ <gr-autocomplete
+ id="groupOwnerInput"
+ .text=${convertToString(this.groupConfig?.owner)}
+ .value=${convertToString(this.groupConfigOwner)}
+ .query=${this.query}
+ ?disabled=${this.computeGroupDisabled()}
+ @text-changed=${this.handleOwnerTextChanged}
+ @value-changed=${this.handleOwnerValueChanged}
+ >
+ </gr-autocomplete>
+ </span>
+ <span class="value" ?disabled=${this.computeGroupDisabled()}>
+ <gr-button
+ id="inputUpdateOwnerBtn"
+ ?disabled=${!groupOwnerNameEdited}
+ @click=${this.handleSaveOwner}
+ >
+ Change Owners</gr-button
+ >
+ </span>
+ </fieldset>
+ `;
+ }
+
+ private renderGroupDescription() {
+ const groupDescriptionEdited =
+ this.originalDescriptionName !== this.groupConfig?.description;
+ return html`
+ <h3 class="heading-3 ${this.computeHeaderClass(groupDescriptionEdited)}">
+ Description
+ </h3>
+ <fieldset>
+ <div>
+ <gr-textarea
+ class="description"
+ autocomplete="on"
+ rows="4"
+ monospace
+ ?disabled=${this.computeGroupDisabled()}
+ .text=${convertToString(this.groupConfig?.description)}
+ @text-changed=${this.handleDescriptionTextChanged}
+ >
+ </div>
+ <span class="value" ?disabled=${this.computeGroupDisabled()}>
+ <gr-button
+ ?disabled=${!groupDescriptionEdited}
+ @click=${this.handleSaveDescription}
+ >
+ Save Description
+ </gr-button>
+ </span>
+ </fieldset>
+ `;
+ }
+
+ private renderGroupOptions() {
+ const groupOptionsEdited =
+ this.originalOptionsVisibleToAll !==
+ this.groupConfig?.options?.visible_to_all;
+ return html`
+ <h3
+ id="options"
+ class="heading-3 ${this.computeHeaderClass(groupOptionsEdited)}"
+ >
+ Group Options
+ </h3>
+ <fieldset>
+ <section>
+ <span class="title">
+ Make group visible to all registered users
+ </span>
+ <span class="value">
+ <gr-select
+ id="visibleToAll"
+ .bindValue="${this.groupConfig?.options?.visible_to_all}"
+ @bind-value-changed=${this.handleOptionsBindValueChanged}
+ >
+ <select ?disabled=${this.computeGroupDisabled()}>
+ ${this.submitTypes.map(
+ item => html`
+ <option value=${item.value}>${item.label}</option>
+ `
+ )}
+ </select>
+ </gr-select>
+ </span>
+ </section>
+ <span class="value" ?disabled=${this.computeGroupDisabled()}>
+ <gr-button
+ ?disabled=${!groupOptionsEdited}
+ @click=${this.handleSaveOptions}
+ >
+ Save Group Options
+ </gr-button>
+ </span>
+ </fieldset>
+ `;
+ }
+
+ /* private but used in test */
+ async loadGroup() {
if (!this.groupId) {
return;
}
@@ -146,154 +321,127 @@
firePageError(response);
};
- return this.restApiService
- .getGroupConfig(this.groupId, errFn)
- .then(config => {
- if (!config || !config.name) {
- return Promise.resolve();
- }
+ const config = await this.restApiService.getGroupConfig(
+ this.groupId,
+ errFn
+ );
+ if (!config || !config.name) return;
- this._groupName = config.name;
- this._groupIsInternal = !!config.id.match(INTERNAL_GROUP_REGEX);
+ if (config.description === undefined) {
+ config.description = '';
+ }
- promises.push(
- this.restApiService.getIsAdmin().then(isAdmin => {
- this._isAdmin = !!isAdmin;
- })
- );
+ this.originalName = config.name;
+ this.originalOwnerName = config.owner;
+ this.originalDescriptionName = config.description;
+ this.groupIsInternal = !!config.id.match(INTERNAL_GROUP_REGEX);
- promises.push(
- this.restApiService.getIsGroupOwner(config.name).then(isOwner => {
- this._groupOwner = !!isOwner;
- })
- );
+ promises.push(
+ this.restApiService.getIsAdmin().then(isAdmin => {
+ this.isAdmin = !!isAdmin;
+ })
+ );
- // If visible to all is undefined, set to false. If it is defined
- // as false, setting to false is fine. If any optional values
- // are added with a default of true, then this would need to be an
- // undefined check and not a truthy/falsy check.
- if (config.options && !config.options.visible_to_all) {
- config.options.visible_to_all = false;
- }
- this._groupConfig = config;
+ promises.push(
+ this.restApiService.getIsGroupOwner(config.name).then(isOwner => {
+ this.groupOwner = !!isOwner;
+ })
+ );
- fireTitleChange(this, config.name);
+ // If visible to all is undefined, set to false. If it is defined
+ // as false, setting to false is fine. If any optional values
+ // are added with a default of true, then this would need to be an
+ // undefined check and not a truthy/falsy check.
+ if (config.options && !config.options.visible_to_all) {
+ config.options.visible_to_all = false;
+ }
+ this.groupConfig = config;
+ this.originalOptionsVisibleToAll = config?.options?.visible_to_all;
- return Promise.all(promises).then(() => {
- this._loading = false;
- });
- });
+ fireTitleChange(this, config.name);
+
+ await Promise.all(promises);
+ this.loading = false;
}
- _computeLoadingClass(loading: boolean) {
- return loading ? 'loading' : '';
+ /* private but used in test */
+ computeLoadingClass() {
+ return this.loading ? 'loading' : '';
}
- _isLoading() {
- return this._loading || this._loading === undefined;
- }
-
- _handleSaveName() {
- const groupConfig = this._groupConfig;
+ /* private but used in test */
+ async handleSaveName() {
+ const groupConfig = this.groupConfig;
if (!this.groupId || !groupConfig || !groupConfig.name) {
return Promise.reject(new Error('invalid groupId or config name'));
}
const groupName = groupConfig.name;
- return this.restApiService
- .saveGroupName(this.groupId, groupName)
- .then(config => {
- if (config.status === 200) {
- this._groupName = groupName;
- const detail: GroupNameChangedDetail = {
- name: groupName,
- external: !this._groupIsInternal,
- };
- fireEvent(this, 'name-changed');
- this.dispatchEvent(
- new CustomEvent('name-changed', {
- detail,
- composed: true,
- bubbles: true,
- })
- );
- this._rename = false;
- }
- });
+ const config = await this.restApiService.saveGroupName(
+ this.groupId,
+ groupName
+ );
+ if (config.status === 200) {
+ this.originalName = groupName;
+ const detail: GroupNameChangedDetail = {
+ name: groupName,
+ external: !this.groupIsInternal,
+ };
+ this.dispatchEvent(
+ new CustomEvent('name-changed', {
+ detail,
+ composed: true,
+ bubbles: true,
+ })
+ );
+ this.requestUpdate();
+ }
+
+ return;
}
- _handleSaveOwner() {
- if (!this.groupId || !this._groupConfig) return;
- let owner = this._groupConfig.owner;
- if (this._groupConfigOwner) {
- owner = decodeURIComponent(this._groupConfigOwner);
+ /* private but used in test */
+ async handleSaveOwner() {
+ if (!this.groupId || !this.groupConfig) return;
+ let owner = this.groupConfig.owner;
+ if (this.groupConfigOwner) {
+ owner = decodeURIComponent(this.groupConfigOwner);
}
if (!owner) return;
- return this.restApiService.saveGroupOwner(this.groupId, owner).then(() => {
- this._owner = false;
- });
+ await this.restApiService.saveGroupOwner(this.groupId, owner);
+ this.originalOwnerName = this.groupConfig?.owner;
+ this.groupConfigOwner = undefined;
}
- _handleSaveDescription() {
- if (!this.groupId || !this._groupConfig || !this._groupConfig.description)
+ /* private but used in test */
+ async handleSaveDescription() {
+ if (
+ !this.groupId ||
+ !this.groupConfig ||
+ this.groupConfig.description === undefined
+ )
return;
- return this.restApiService
- .saveGroupDescription(this.groupId, this._groupConfig.description)
- .then(() => {
- this._description = false;
- });
+ await this.restApiService.saveGroupDescription(
+ this.groupId,
+ this.groupConfig.description
+ );
+ this.originalDescriptionName = this.groupConfig.description;
}
- _handleSaveOptions() {
- if (!this.groupId || !this._groupConfig || !this._groupConfig.options)
- return;
- const visible = this._groupConfig.options.visible_to_all;
-
+ /* private but used in test */
+ async handleSaveOptions() {
+ if (!this.groupId || !this.groupConfig || !this.groupConfig.options) return;
+ const visible = this.groupConfig.options.visible_to_all;
const options = {visible_to_all: visible};
-
- return this.restApiService
- .saveGroupOptions(this.groupId, options)
- .then(() => {
- this._options = false;
- });
+ await this.restApiService.saveGroupOptions(this.groupId, options);
+ this.originalOptionsVisibleToAll =
+ this.groupConfig?.options?.visible_to_all;
}
- @observe('_groupConfig.name')
- _handleConfigName() {
- if (this._isLoading()) {
- return;
- }
- this._rename = true;
- }
-
- @observe('_groupConfig.owner', '_groupConfigOwner')
- _handleConfigOwner() {
- if (this._isLoading()) {
- return;
- }
- this._owner = true;
- }
-
- @observe('_groupConfig.description')
- _handleConfigDescription() {
- if (this._isLoading()) {
- return;
- }
- this._description = true;
- }
-
- @observe('_groupConfig.options.visible_to_all')
- _handleConfigOptions() {
- if (this._isLoading()) {
- return;
- }
- this._options = true;
- }
-
- _computeHeaderClass(configChanged: boolean) {
+ private computeHeaderClass(configChanged: boolean) {
return configChanged ? 'edited' : '';
}
- _getGroupSuggestions(input: string) {
+ private getGroupSuggestions(input: string) {
return this.restApiService.getSuggestedGroups(input).then(response => {
const groups: AutocompleteSuggestion[] = [];
for (const [name, group] of Object.entries(response ?? {})) {
@@ -303,17 +451,45 @@
});
}
- _computeGroupDisabled(
- owner: boolean,
- admin: boolean,
- groupIsInternal: boolean
- ) {
- return !(groupIsInternal && (admin || owner));
+ /* private but used in test */
+ computeGroupDisabled() {
+ return !(this.groupIsInternal && (this.isAdmin || this.groupOwner));
}
- _getGroupUUID(id: GroupId) {
+ private getGroupUUID() {
+ const id = this.groupConfig?.id;
if (!id) return;
-
return id.match(INTERNAL_GROUP_REGEX) ? id : decodeURIComponent(id);
}
+
+ private handleNameTextChanged(e: CustomEvent) {
+ if (!this.groupConfig || this.loading) return;
+ this.groupConfig.name = e.detail.value as GroupName;
+ this.requestUpdate();
+ }
+
+ private handleOwnerTextChanged(e: CustomEvent) {
+ if (!this.groupConfig || this.loading) return;
+ this.groupConfig.owner = e.detail.value;
+ this.requestUpdate();
+ }
+
+ private handleOwnerValueChanged(e: CustomEvent) {
+ if (this.loading) return;
+ this.groupConfigOwner = e.detail.value;
+ this.requestUpdate();
+ }
+
+ private handleDescriptionTextChanged(e: CustomEvent) {
+ if (!this.groupConfig || this.loading) return;
+ this.groupConfig.description = e.detail.value;
+ this.requestUpdate();
+ }
+
+ private handleOptionsBindValueChanged(e: BindValueChangeEvent) {
+ if (!this.groupConfig || !this.groupConfig.options || this.loading) return;
+ this.groupConfig.options.visible_to_all = e.detail
+ .value as unknown as boolean;
+ this.requestUpdate();
+ }
}
diff --git a/polygerrit-ui/app/elements/admin/gr-group/gr-group_html.ts b/polygerrit-ui/app/elements/admin/gr-group/gr-group_html.ts
deleted file mode 100644
index 6bc5d2a..0000000
--- a/polygerrit-ui/app/elements/admin/gr-group/gr-group_html.ts
+++ /dev/null
@@ -1,168 +0,0 @@
-/**
- * @license
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-import {html} from '@polymer/polymer/lib/utils/html-tag';
-
-export const htmlTemplate = html`
- <style include="gr-font-styles">
- /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
- </style>
- <style include="shared-styles">
- /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
- </style>
- <style include="gr-subpage-styles">
- h3.edited:after {
- color: var(--deemphasized-text-color);
- content: ' *';
- }
- </style>
- <style include="gr-form-styles">
- /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
- </style>
- <div class="main gr-form-styles read-only">
- <div id="loading" class$="[[_computeLoadingClass(_loading)]]">
- Loading...
- </div>
- <div id="loadedContent" class$="[[_computeLoadingClass(_loading)]]">
- <h1 id="Title" class="heading-1">[[_groupName]]</h1>
- <h2 id="configurations" class="heading-2">General</h2>
- <div id="form">
- <fieldset>
- <h3 id="groupUUID" class="heading-3">Group UUID</h3>
- <fieldset>
- <gr-copy-clipboard
- id="uuid"
- text="[[_getGroupUUID(_groupConfig.id)]]"
- ></gr-copy-clipboard>
- </fieldset>
- <h3
- id="groupName"
- class$="heading-3 [[_computeHeaderClass(_rename)]]"
- >
- Group Name
- </h3>
- <fieldset>
- <span class="value">
- <gr-autocomplete
- id="groupNameInput"
- text="{{_groupConfig.name}}"
- disabled="[[_computeGroupDisabled(_groupOwner, _isAdmin, _groupIsInternal)]]"
- ></gr-autocomplete>
- </span>
- <span
- class="value"
- disabled$="[[_computeGroupDisabled(_groupOwner, _isAdmin, _groupIsInternal)]]"
- >
- <gr-button
- id="inputUpdateNameBtn"
- on-click="_handleSaveName"
- disabled="[[!_rename]]"
- >
- Rename Group</gr-button
- >
- </span>
- </fieldset>
- <h3
- id="groupOwner"
- class$="heading-3 [[_computeHeaderClass(_owner)]]"
- >
- Owners
- </h3>
- <fieldset>
- <span class="value">
- <gr-autocomplete
- id="groupOwnerInput"
- text="{{_groupConfig.owner}}"
- value="{{_groupConfigOwner}}"
- query="[[_query]]"
- disabled="[[_computeGroupDisabled(_groupOwner, _isAdmin, _groupIsInternal)]]"
- >
- </gr-autocomplete>
- </span>
- <span
- class="value"
- disabled$="[[_computeGroupDisabled(_groupOwner, _isAdmin, _groupIsInternal)]]"
- >
- <gr-button
- id="inputUpdateOwnerBtn"
- on-click="_handleSaveOwner"
- disabled="[[!_owner]]"
- >
- Change Owners</gr-button
- >
- </span>
- </fieldset>
- <h3 class$="heading-3 [[_computeHeaderClass(_description)]]">
- Description
- </h3>
- <fieldset>
- <div>
- <iron-autogrow-textarea
- class="description"
- autocomplete="on"
- bind-value="{{_groupConfig.description}}"
- disabled="[[_computeGroupDisabled(_groupOwner, _isAdmin, _groupIsInternal)]]"
- ></iron-autogrow-textarea>
- </div>
- <span
- class="value"
- disabled$="[[_computeGroupDisabled(_groupOwner, _isAdmin, _groupIsInternal)]]"
- >
- <gr-button
- on-click="_handleSaveDescription"
- disabled="[[!_description]]"
- >
- Save Description
- </gr-button>
- </span>
- </fieldset>
- <h3 id="options" class$="heading-3 [[_computeHeaderClass(_options)]]">
- Group Options
- </h3>
- <fieldset>
- <section>
- <span class="title">
- Make group visible to all registered users
- </span>
- <span class="value">
- <gr-select
- id="visibleToAll"
- bind-value="{{_groupConfig.options.visible_to_all}}"
- >
- <select
- disabled$="[[_computeGroupDisabled(_groupOwner, _isAdmin, _groupIsInternal)]]"
- >
- <template is="dom-repeat" items="[[_submitTypes]]">
- <option value="[[item.value]]">[[item.label]]</option>
- </template>
- </select>
- </gr-select>
- </span>
- </section>
- <span
- class="value"
- disabled$="[[_computeGroupDisabled(_groupOwner, _isAdmin, _groupIsInternal)]]"
- >
- <gr-button on-click="_handleSaveOptions" disabled="[[!_options]]">
- Save Group Options
- </gr-button>
- </span>
- </fieldset>
- </fieldset>
- </div>
- </div>
- </div>
-`;
diff --git a/polygerrit-ui/app/elements/admin/gr-group/gr-group_test.js b/polygerrit-ui/app/elements/admin/gr-group/gr-group_test.js
deleted file mode 100644
index e390ac5..0000000
--- a/polygerrit-ui/app/elements/admin/gr-group/gr-group_test.js
+++ /dev/null
@@ -1,234 +0,0 @@
-/**
- * @license
- * Copyright (C) 2017 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-import '../../../test/common-test-setup-karma.js';
-import './gr-group.js';
-import {
- addListenerForTest,
- mockPromise,
- stubRestApi,
-} from '../../../test/test-utils.js';
-
-const basicFixture = fixtureFromElement('gr-group');
-
-suite('gr-group tests', () => {
- let element;
-
- let groupStub;
- const group = {
- id: '6a1e70e1a88782771a91808c8af9bbb7a9871389',
- url: '#/admin/groups/uuid-6a1e70e1a88782771a91808c8af9bbb7a9871389',
- options: {},
- description: 'Gerrit Site Administrators',
- group_id: 1,
- owner: 'Administrators',
- owner_id: '6a1e70e1a88782771a91808c8af9bbb7a9871389',
- name: 'Administrators',
- };
-
- setup(() => {
- element = basicFixture.instantiate();
- groupStub = stubRestApi('getGroupConfig').returns(Promise.resolve(group));
- });
-
- test('loading displays before group config is loaded', () => {
- assert.isTrue(element.$.loading.classList.contains('loading'));
- assert.isFalse(getComputedStyle(element.$.loading).display === 'none');
- assert.isTrue(element.$.loadedContent.classList.contains('loading'));
- assert.isTrue(getComputedStyle(element.$.loadedContent)
- .display === 'none');
- });
-
- test('default values are populated with internal group', async () => {
- stubRestApi('getIsGroupOwner').returns(Promise.resolve(true));
- element.groupId = 1;
- await element._loadGroup();
- assert.isTrue(element._groupIsInternal);
- assert.isFalse(element.$.visibleToAll.bindValue);
- });
-
- test('default values with external group', async () => {
- const groupExternal = {...group};
- groupExternal.id = 'external-group-id';
- groupStub.restore();
- groupStub = stubRestApi('getGroupConfig').returns(
- Promise.resolve(groupExternal));
- stubRestApi('getIsGroupOwner').returns(Promise.resolve(true));
- element.groupId = 1;
- await element._loadGroup();
- assert.isFalse(element._groupIsInternal);
- assert.isFalse(element.$.visibleToAll.bindValue);
- });
-
- test('rename group', async () => {
- const groupName = 'test-group';
- const groupName2 = 'test-group2';
- element.groupId = 1;
- element._groupConfig = {
- name: groupName,
- };
- element._groupName = groupName;
-
- stubRestApi('getIsGroupOwner').returns(Promise.resolve(true));
- stubRestApi('saveGroupName').returns(Promise.resolve({status: 200}));
-
- const button = element.$.inputUpdateNameBtn;
-
- await element._loadGroup();
- assert.isTrue(button.hasAttribute('disabled'));
- assert.isFalse(element.$.Title.classList.contains('edited'));
-
- element.$.groupNameInput.text = groupName2;
-
- await flush();
- assert.isFalse(button.hasAttribute('disabled'));
- assert.isTrue(element.$.groupName.classList.contains('edited'));
-
- await element._handleSaveName();
- assert.isTrue(button.hasAttribute('disabled'));
- assert.isFalse(element.$.Title.classList.contains('edited'));
- assert.equal(element._groupName, groupName2);
- });
-
- test('rename group owner', async () => {
- const groupName = 'test-group';
- element.groupId = 1;
- element._groupConfig = {
- name: groupName,
- };
- element._groupConfigOwner = 'testId';
- element._groupOwner = true;
-
- stubRestApi('getIsGroupOwner').returns(Promise.resolve({status: 200}));
-
- const button = element.$.inputUpdateOwnerBtn;
-
- await element._loadGroup();
- assert.isTrue(button.hasAttribute('disabled'));
- assert.isFalse(element.$.Title.classList.contains('edited'));
-
- element.$.groupOwnerInput.text = 'testId2';
-
- await flush();
- assert.isFalse(button.hasAttribute('disabled'));
- assert.isTrue(element.$.groupOwner.classList.contains('edited'));
-
- await element._handleSaveOwner();
- assert.isTrue(button.hasAttribute('disabled'));
- assert.isFalse(element.$.Title.classList.contains('edited'));
- });
-
- test('test for undefined group name', async () => {
- groupStub.restore();
-
- stubRestApi('getGroupConfig').returns(Promise.resolve({}));
-
- assert.isUndefined(element.groupId);
-
- element.groupId = 1;
-
- assert.isDefined(element.groupId);
-
- // Test that loading shows instead of filling
- // in group details
- await element._loadGroup();
- assert.isTrue(element.$.loading.classList.contains('loading'));
-
- assert.isTrue(element._loading);
- });
-
- test('test fire event', async () => {
- element._groupConfig = {
- name: 'test-group',
- };
- element.groupId = 'gg';
- stubRestApi('saveGroupName').returns(Promise.resolve({status: 200}));
-
- const showStub = sinon.stub(element, 'dispatchEvent');
- await element._handleSaveName();
- assert.isTrue(showStub.called);
- });
-
- test('_computeGroupDisabled', () => {
- let admin = true;
- let owner = false;
- let groupIsInternal = true;
- assert.equal(element._computeGroupDisabled(owner, admin,
- groupIsInternal), false);
-
- admin = false;
- assert.equal(element._computeGroupDisabled(owner, admin,
- groupIsInternal), true);
-
- owner = true;
- assert.equal(element._computeGroupDisabled(owner, admin,
- groupIsInternal), false);
-
- owner = false;
- assert.equal(element._computeGroupDisabled(owner, admin,
- groupIsInternal), true);
-
- groupIsInternal = false;
- assert.equal(element._computeGroupDisabled(owner, admin,
- groupIsInternal), true);
-
- admin = true;
- assert.equal(element._computeGroupDisabled(owner, admin,
- groupIsInternal), true);
- });
-
- test('_computeLoadingClass', () => {
- assert.equal(element._computeLoadingClass(true), 'loading');
- assert.equal(element._computeLoadingClass(false), '');
- });
-
- test('fires page-error', async () => {
- groupStub.restore();
-
- element.groupId = 1;
-
- const response = {status: 404};
- stubRestApi('getGroupConfig').callsFake((group, errFn) => {
- errFn(response);
- return Promise.resolve(undefined);
- });
-
- const promise = mockPromise();
- addListenerForTest(document, 'page-error', e => {
- assert.deepEqual(e.detail.response, response);
- promise.resolve();
- });
-
- element._loadGroup();
- await promise;
- });
-
- test('uuid', () => {
- element._groupConfig = {
- id: '6a1e70e1a88782771a91808c8af9bbb7a9871389',
- };
-
- assert.equal(element._groupConfig.id, element.$.uuid.text);
-
- element._groupConfig = {
- id: 'user%2Fgroup',
- };
-
- assert.equal('user/group', element.$.uuid.text);
- });
-});
-
diff --git a/polygerrit-ui/app/elements/admin/gr-group/gr-group_test.ts b/polygerrit-ui/app/elements/admin/gr-group/gr-group_test.ts
new file mode 100644
index 0000000..5e96e33
--- /dev/null
+++ b/polygerrit-ui/app/elements/admin/gr-group/gr-group_test.ts
@@ -0,0 +1,314 @@
+/**
+ * @license
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import '../../../test/common-test-setup-karma';
+import './gr-group';
+import {GrGroup} from './gr-group';
+import {
+ addListenerForTest,
+ mockPromise,
+ queryAndAssert,
+ stubRestApi,
+} from '../../../test/test-utils';
+import {createGroupInfo} from '../../../test/test-data-generators.js';
+import {GroupId, GroupInfo, GroupName} from '../../../types/common';
+import {GrAutocomplete} from '../../shared/gr-autocomplete/gr-autocomplete';
+import {GrButton} from '../../shared/gr-button/gr-button';
+import {GrCopyClipboard} from '../../shared/gr-copy-clipboard/gr-copy-clipboard';
+import {GrSelect} from '../../shared/gr-select/gr-select';
+
+const basicFixture = fixtureFromElement('gr-group');
+
+suite('gr-group tests', () => {
+ let element: GrGroup;
+ let groupStub: sinon.SinonStub;
+
+ const group: GroupInfo = {
+ ...createGroupInfo('6a1e70e1a88782771a91808c8af9bbb7a9871389'),
+ url: '#/admin/groups/uuid-6a1e70e1a88782771a91808c8af9bbb7a9871389',
+ options: {
+ visible_to_all: false,
+ },
+ description: 'Gerrit Site Administrators',
+ group_id: 1,
+ owner: 'Administrators',
+ owner_id: '6a1e70e1a88782771a91808c8af9bbb7a9871389',
+ name: 'Administrators' as GroupName,
+ };
+
+ setup(async () => {
+ element = basicFixture.instantiate();
+ await element.updateComplete;
+ groupStub = stubRestApi('getGroupConfig').returns(Promise.resolve(group));
+ });
+
+ test('loading displays before group config is loaded', () => {
+ assert.isTrue(
+ queryAndAssert<HTMLDivElement>(element, '#loading').classList.contains(
+ 'loading'
+ )
+ );
+ assert.isFalse(
+ getComputedStyle(queryAndAssert<HTMLDivElement>(element, '#loading'))
+ .display === 'none'
+ );
+ assert.isTrue(
+ queryAndAssert<HTMLDivElement>(
+ element,
+ '#loadedContent'
+ ).classList.contains('loading')
+ );
+ assert.isTrue(
+ getComputedStyle(
+ queryAndAssert<HTMLDivElement>(element, '#loadedContent')
+ ).display === 'none'
+ );
+ });
+
+ test('default values are populated with internal group', async () => {
+ stubRestApi('getIsGroupOwner').returns(Promise.resolve(true));
+ element.groupId = '1' as GroupId;
+ await element.loadGroup();
+ assert.isTrue(element.groupIsInternal);
+ assert.isFalse(
+ queryAndAssert<GrSelect>(element, '#visibleToAll').bindValue
+ );
+ });
+
+ test('default values with external group', async () => {
+ const groupExternal = {...group};
+ groupExternal.id = 'external-group-id' as GroupId;
+ groupStub.restore();
+ groupStub = stubRestApi('getGroupConfig').returns(
+ Promise.resolve(groupExternal)
+ );
+ stubRestApi('getIsGroupOwner').returns(Promise.resolve(true));
+ element.groupId = '1' as GroupId;
+ await element.loadGroup();
+ assert.isFalse(element.groupIsInternal);
+ assert.isFalse(
+ queryAndAssert<GrSelect>(element, '#visibleToAll').bindValue
+ );
+ });
+
+ test('rename group', async () => {
+ const groupName = 'test-group';
+ const groupName2 = 'test-group2';
+ element.groupId = '1' as GroupId;
+ element.groupConfig = {
+ name: groupName as GroupName,
+ id: '1' as GroupId,
+ };
+ element.originalName = groupName as GroupName;
+
+ stubRestApi('getIsGroupOwner').returns(Promise.resolve(true));
+ stubRestApi('saveGroupName').returns(
+ Promise.resolve({...new Response(), status: 200})
+ );
+
+ const button = queryAndAssert<GrButton>(element, '#inputUpdateNameBtn');
+
+ await element.loadGroup();
+ assert.isTrue(button.hasAttribute('disabled'));
+ assert.isFalse(
+ queryAndAssert<HTMLHeadingElement>(element, '#Title').classList.contains(
+ 'edited'
+ )
+ );
+
+ queryAndAssert<GrAutocomplete>(element, '#groupNameInput').text =
+ groupName2;
+
+ await element.updateComplete;
+
+ assert.isFalse(button.hasAttribute('disabled'));
+ assert.isTrue(
+ queryAndAssert<HTMLHeadingElement>(
+ element,
+ '#groupName'
+ ).classList.contains('edited')
+ );
+
+ await element.handleSaveName();
+ assert.isTrue(button.disabled);
+ assert.isFalse(
+ queryAndAssert<HTMLHeadingElement>(element, '#Title').classList.contains(
+ 'edited'
+ )
+ );
+ assert.equal(element.originalName, groupName2);
+ });
+
+ test('rename group owner', async () => {
+ const groupName = 'test-group';
+ element.groupId = '1' as GroupId;
+ element.groupConfig = {
+ name: groupName as GroupName,
+ id: '1' as GroupId,
+ };
+ element.groupConfigOwner = 'testId';
+ element.groupOwner = true;
+
+ stubRestApi('getIsGroupOwner').returns(Promise.resolve(true));
+
+ const button = queryAndAssert<GrButton>(element, '#inputUpdateOwnerBtn');
+
+ await element.loadGroup();
+ assert.isTrue(button.disabled);
+ assert.isFalse(
+ queryAndAssert<HTMLHeadingElement>(element, '#Title').classList.contains(
+ 'edited'
+ )
+ );
+
+ queryAndAssert<GrAutocomplete>(element, '#groupOwnerInput').text =
+ 'testId2';
+
+ await element.updateComplete;
+ assert.isFalse(button.disabled);
+ assert.isTrue(
+ queryAndAssert<HTMLHeadingElement>(
+ element,
+ '#groupOwner'
+ ).classList.contains('edited')
+ );
+
+ await element.handleSaveOwner();
+ assert.isTrue(button.disabled);
+ assert.isFalse(
+ queryAndAssert<HTMLHeadingElement>(element, '#Title').classList.contains(
+ 'edited'
+ )
+ );
+ });
+
+ test('test for undefined group name', async () => {
+ groupStub.restore();
+
+ stubRestApi('getGroupConfig').returns(Promise.resolve(undefined));
+
+ assert.isUndefined(element.groupId);
+
+ element.groupId = '1' as GroupId;
+
+ assert.isDefined(element.groupId);
+
+ // Test that loading shows instead of filling
+ // in group details
+ await element.loadGroup();
+ assert.isTrue(
+ queryAndAssert<HTMLDivElement>(element, '#loading').classList.contains(
+ 'loading'
+ )
+ );
+
+ assert.isTrue(element.loading);
+ });
+
+ test('test fire event', async () => {
+ element.groupConfig = {
+ name: 'test-group' as GroupName,
+ id: '1' as GroupId,
+ };
+ element.groupId = 'gg' as GroupId;
+ stubRestApi('saveGroupName').returns(
+ Promise.resolve({...new Response(), status: 200})
+ );
+
+ const showStub = sinon.stub(element, 'dispatchEvent');
+ await element.handleSaveName();
+ assert.isTrue(showStub.called);
+ });
+
+ test('computeGroupDisabled', () => {
+ element.isAdmin = true;
+ element.groupOwner = false;
+ element.groupIsInternal = true;
+ assert.equal(element.computeGroupDisabled(), false);
+
+ element.isAdmin = false;
+ assert.equal(element.computeGroupDisabled(), true);
+
+ element.groupOwner = true;
+ assert.equal(element.computeGroupDisabled(), false);
+
+ element.groupOwner = false;
+ assert.equal(element.computeGroupDisabled(), true);
+
+ element.groupIsInternal = false;
+ assert.equal(element.computeGroupDisabled(), true);
+
+ element.isAdmin = true;
+ assert.equal(element.computeGroupDisabled(), true);
+ });
+
+ test('computeLoadingClass', () => {
+ element.loading = true;
+ assert.equal(element.computeLoadingClass(), 'loading');
+ element.loading = false;
+ assert.equal(element.computeLoadingClass(), '');
+ });
+
+ test('fires page-error', async () => {
+ groupStub.restore();
+
+ element.groupId = '1' as GroupId;
+
+ const response = {...new Response(), status: 404};
+ stubRestApi('getGroupConfig').callsFake((_, errFn) => {
+ if (errFn !== undefined) {
+ errFn(response);
+ } else {
+ assert.fail('errFn is undefined');
+ }
+ return Promise.resolve(undefined);
+ });
+
+ const promise = mockPromise();
+ addListenerForTest(document, 'page-error', e => {
+ assert.deepEqual((e as CustomEvent).detail.response, response);
+ promise.resolve();
+ });
+
+ await element.loadGroup();
+ await promise;
+ });
+
+ test('uuid', async () => {
+ element.groupConfig = {
+ id: '6a1e70e1a88782771a91808c8af9bbb7a9871389' as GroupId,
+ };
+
+ await element.updateComplete;
+
+ assert.equal(
+ element.groupConfig.id,
+ queryAndAssert<GrCopyClipboard>(element, '#uuid').text
+ );
+
+ element.groupConfig = {
+ id: 'user%2Fgroup' as GroupId,
+ };
+
+ await element.updateComplete;
+
+ assert.equal(
+ 'user/group',
+ queryAndAssert<GrCopyClipboard>(element, '#uuid').text
+ );
+ });
+});
diff --git a/polygerrit-ui/app/elements/admin/gr-permission/gr-permission.ts b/polygerrit-ui/app/elements/admin/gr-permission/gr-permission.ts
index 0c34a84..b7ea237 100644
--- a/polygerrit-ui/app/elements/admin/gr-permission/gr-permission.ts
+++ b/polygerrit-ui/app/elements/admin/gr-permission/gr-permission.ts
@@ -18,6 +18,7 @@
import '@polymer/paper-toggle-button/paper-toggle-button';
import '../../../styles/gr-form-styles';
import '../../../styles/gr-menu-page-styles';
+import '../../../styles/gr-paper-styles';
import '../../../styles/shared-styles';
import '../../shared/gr-autocomplete/gr-autocomplete';
import '../../shared/gr-button/gr-button';
diff --git a/polygerrit-ui/app/elements/admin/gr-permission/gr-permission_html.ts b/polygerrit-ui/app/elements/admin/gr-permission/gr-permission_html.ts
index 3559194..a8405df 100644
--- a/polygerrit-ui/app/elements/admin/gr-permission/gr-permission_html.ts
+++ b/polygerrit-ui/app/elements/admin/gr-permission/gr-permission_html.ts
@@ -70,6 +70,9 @@
display: block;
}
</style>
+ <style include="gr-paper-styles">
+ /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
+ </style>
<style include="gr-form-styles">
/* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
</style>
diff --git a/polygerrit-ui/app/elements/admin/gr-repo-plugin-config/gr-repo-plugin-config.ts b/polygerrit-ui/app/elements/admin/gr-repo-plugin-config/gr-repo-plugin-config.ts
index 32812dd..7092c9b 100644
--- a/polygerrit-ui/app/elements/admin/gr-repo-plugin-config/gr-repo-plugin-config.ts
+++ b/polygerrit-ui/app/elements/admin/gr-repo-plugin-config/gr-repo-plugin-config.ts
@@ -36,6 +36,7 @@
PluginConfigOptionsChangedEventDetail,
PluginOption,
} from './gr-repo-plugin-config-types';
+import {paperStyles} from '../../../styles/gr-paper-styles';
const PLUGIN_CONFIG_CHANGED_EVENT_NAME = 'plugin-config-changed';
@@ -71,6 +72,7 @@
return [
sharedStyles,
formStyles,
+ paperStyles,
subpageStyles,
css`
.inherited {
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list-column/gr-change-list-column.ts b/polygerrit-ui/app/elements/change-list/gr-change-list-column/gr-change-list-column.ts
index 2b9abfd..b5dee28 100644
--- a/polygerrit-ui/app/elements/change-list/gr-change-list-column/gr-change-list-column.ts
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list-column/gr-change-list-column.ts
@@ -20,6 +20,7 @@
import {customElement, property} from 'lit/decorators';
import {ChangeInfo, SubmitRequirementStatus} from '../../../api/rest-api';
import {changeIsMerged} from '../../../utils/change-util';
+import {getRequirements} from '../../../utils/label-util';
@customElement('gr-change-list-column-requirements')
export class GrChangeListColumRequirements extends LitElement {
@@ -52,13 +53,13 @@
return this.renderState('check', 'Merged');
}
- const submitRequirements = (this.change?.submit_requirements ?? []).filter(
- req => req.status !== SubmitRequirementStatus.NOT_APPLICABLE
- );
+ const submitRequirements = getRequirements(this.change);
if (!submitRequirements.length) return html`n/a`;
const numOfRequirements = submitRequirements.length;
const numOfSatisfiedRequirements = submitRequirements.filter(
- req => req.status === SubmitRequirementStatus.SATISFIED
+ req =>
+ req.status === SubmitRequirementStatus.SATISFIED ||
+ req.status === SubmitRequirementStatus.OVERRIDDEN
).length;
if (numOfSatisfiedRequirements === numOfRequirements) {
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item_html.ts b/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item_html.ts
index c59b2e3..cd55e15 100644
--- a/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item_html.ts
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item_html.ts
@@ -46,7 +46,8 @@
width: 100%;
}
.comments,
- .reviewers {
+ .reviewers,
+ .requirements {
white-space: nowrap;
}
.reviewers {
@@ -120,7 +121,7 @@
</style>
<td aria-hidden="true" class="cell leftPadding"></td>
<td class="cell star" hidden$="[[!showStar]]" hidden="">
- <gr-change-star change="{{change}}"></gr-change-star>
+ <gr-change-star change="[[change]]"></gr-change-star>
</td>
<td class="cell number" hidden$="[[!showNumber]]" hidden="">
<a href$="[[changeURL]]">[[change._number]]</a>
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list.ts b/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list.ts
index 66cef20..68566f0 100644
--- a/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list.ts
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list.ts
@@ -16,9 +16,10 @@
*/
import '../../../styles/gr-change-list-styles';
+import '../../../styles/gr-font-styles';
+import '../../../styles/shared-styles';
import '../../shared/gr-cursor-manager/gr-cursor-manager';
import '../gr-change-list-item/gr-change-list-item';
-import '../../../styles/shared-styles';
import '../../plugins/gr-endpoint-decorator/gr-endpoint-decorator';
import {afterNextRender} from '@polymer/polymer/lib/utils/render-status';
import {PolymerElement} from '@polymer/polymer/polymer-element';
@@ -51,6 +52,7 @@
import {ScrollMode} from '../../../constants/constants';
import {listen} from '../../../services/shortcuts/shortcuts-service';
import {KnownExperimentId} from '../../../services/flags/flags';
+import {PRIORITY_REQUIREMENTS_ORDER} from '../../../utils/label-util';
const NUMBER_FIXED_COLUMNS = 3;
const CLOSED_STATUS = ['MERGED', 'ABANDONED'];
@@ -320,6 +322,13 @@
labels = labels.concat(currentLabels.filter(nonExistingLabel));
}
}
+ if (
+ this.flagsService.enabledExperiments.includes(
+ KnownExperimentId.SUBMIT_REQUIREMENTS_UI
+ )
+ ) {
+ labels = labels.filter(l => PRIORITY_REQUIREMENTS_ORDER.includes(l));
+ }
return labels.sort();
}
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list_html.ts b/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list_html.ts
index c31da77..77320b9 100644
--- a/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list_html.ts
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list_html.ts
@@ -20,6 +20,9 @@
<style include="shared-styles">
/* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
</style>
+ <style include="gr-font-styles">
+ /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
+ </style>
<style include="gr-change-list-styles">
#changeList {
border-collapse: collapse;
diff --git a/polygerrit-ui/app/elements/change/gr-change-summary/gr-change-summary.ts b/polygerrit-ui/app/elements/change/gr-change-summary/gr-change-summary.ts
index ad8f72f..e4b466b 100644
--- a/polygerrit-ui/app/elements/change/gr-change-summary/gr-change-summary.ts
+++ b/polygerrit-ui/app/elements/change/gr-change-summary/gr-change-summary.ts
@@ -232,6 +232,9 @@
height: var(--line-height-small);
vertical-align: top;
}
+ .checksChip a iron-icon.launch {
+ color: var(--link-color);
+ }
.checksChip.error {
color: var(--error-foreground);
border-color: var(--error-foreground);
diff --git a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.ts b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.ts
index 1a4b7a0..8d64bc0 100644
--- a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.ts
+++ b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.ts
@@ -16,6 +16,7 @@
*/
import '@polymer/paper-tabs/paper-tabs';
import '../../../styles/gr-a11y-styles';
+import '../../../styles/gr-paper-styles';
import '../../../styles/shared-styles';
import '../../diff/gr-comment-api/gr-comment-api';
import '../../plugins/gr-endpoint-decorator/gr-endpoint-decorator';
@@ -42,6 +43,7 @@
import '../gr-reply-dialog/gr-reply-dialog';
import '../gr-thread-list/gr-thread-list';
import '../../checks/gr-checks-tab';
+import {ChangeStarToggleStarDetail} from '../../shared/gr-change-star/gr-change-star';
import {flush} from '@polymer/polymer/lib/legacy/polymer.dom';
import {PolymerElement} from '@polymer/polymer/polymer-element';
import {htmlTemplate} from './gr-change-view_html';
@@ -61,12 +63,12 @@
import {getPluginEndpoints} from '../../shared/gr-js-api-interface/gr-plugin-endpoints';
import {getPluginLoader} from '../../shared/gr-js-api-interface/gr-plugin-loader';
import {RevisionInfo as RevisionInfoClass} from '../../shared/revision-info/revision-info';
-import {DiffViewMode} from '../../../api/diff';
import {
ChangeStatus,
DefaultBase,
PrimaryTab,
SecondaryTab,
+ DiffViewMode,
} from '../../../constants/constants';
import {NO_ROBOT_COMMENTS_THREADS_MSG} from '../../../constants/messages';
@@ -131,7 +133,11 @@
ChangeComments,
GrCommentApi,
} from '../../diff/gr-comment-api/gr-comment-api';
-import {assertIsDefined, hasOwnProperty} from '../../../utils/common-util';
+import {
+ assertIsDefined,
+ hasOwnProperty,
+ query,
+} from '../../../utils/common-util';
import {GrEditControls} from '../../edit/gr-edit-controls/gr-edit-controls';
import {
CommentThread,
@@ -234,7 +240,6 @@
downloadOverlay: GrOverlay;
downloadDialog: GrDownloadDialog;
replyOverlay: GrOverlay;
- replyDialog: GrReplyDialog;
mainContent: HTMLDivElement;
changeStar: GrChangeStar;
actions: GrChangeActions;
@@ -519,7 +524,7 @@
_activeTabs: string[] = [PrimaryTab.FILES, SecondaryTab.CHANGE_LOG];
@property({type: Boolean})
- unresolvedOnly = false;
+ unresolvedOnly = true;
@property({type: Boolean})
_showAllRobotComments = false;
@@ -544,6 +549,10 @@
@property({type: String})
scrollCommentId?: UrlEncodedCommentId;
+ /** Just reflects the `opened` prop of the overlay. */
+ @property({type: Boolean})
+ replyOverlayOpened = false;
+
@property({
type: Array,
computed: '_computeResolveWeblinks(_change, _commitInfo, _serverConfig)',
@@ -552,12 +561,12 @@
restApiService = appContext.restApiService;
+ private readonly userService = appContext.userService;
+
private readonly commentsService = appContext.commentsService;
private readonly shortcuts = appContext.shortcutsService;
- private replyDialogResizeObserver?: ResizeObserver;
-
override keyboardShortcuts(): ShortcutListener[] {
return [
listen(Shortcut.SEND_REPLY, _ => {}), // docOnly
@@ -612,10 +621,14 @@
private lastStarredTimestamp?: number;
- private readonly userService = appContext.userService;
-
private diffViewMode?: DiffViewMode;
+ /**
+ * If the user comes back to the change page we want to remember the scroll
+ * position when we re-render the page as is.
+ */
+ private scrollPosition?: number;
+
override ready() {
super.ready();
aPluginHasRegistered$.pipe(takeUntil(this.disconnected$)).subscribe(b => {
@@ -678,11 +691,6 @@
}
});
- this.replyDialogResizeObserver = new ResizeObserver(() =>
- this.$.replyOverlay.center()
- );
- this.replyDialogResizeObserver.observe(this.$.replyDialog);
-
getPluginLoader()
.awaitPluginsLoaded()
.then(() => {
@@ -709,6 +717,7 @@
this.addEventListener('open-fix-preview', e => this._onOpenFixPreview(e));
this.addEventListener('close-fix-preview', e => this._onCloseFixPreview(e));
document.addEventListener('visibilitychange', this.handleVisibilityChange);
+ document.addEventListener('scroll', this.handleScroll);
this.addEventListener(EventType.SHOW_PRIMARY_TAB, e =>
this._setActivePrimaryTab(e)
@@ -727,6 +736,7 @@
'visibilitychange',
this.handleVisibilityChange
);
+ document.removeEventListener('scroll', this.handleScroll);
this.replyRefitTask?.cancel();
this.scrollTask?.cancel();
@@ -744,6 +754,15 @@
return this.shadowRoot!.querySelector<GrThreadList>('gr-thread-list');
}
+ private readonly handleScroll = () => {
+ if (!this.isViewCurrent) return;
+ this.scrollTask = debounce(
+ this.scrollTask,
+ () => (this.scrollPosition = document.documentElement.scrollTop),
+ 150
+ );
+ };
+
_onOpenFixPreview(e: OpenFixPreviewEvent) {
this.$.applyFixDialog.open(e);
}
@@ -865,7 +884,7 @@
const hasUnresolvedThreads =
(this._commentThreads ?? []).filter(thread => isUnresolved(thread))
.length > 0;
- if (hasUnresolvedThreads) this.unresolvedOnly = true;
+ if (!hasUnresolvedThreads) this.unresolvedOnly = false;
}
this.reporting.reportInteraction(Interaction.SHOW_TAB, {
@@ -1055,7 +1074,7 @@
_handleReplyTap(e: MouseEvent) {
e.preventDefault();
- this._openReplyDialog(this.$.replyDialog.FocusTarget.ANY);
+ this._openReplyDialog(FocusTarget.ANY);
}
onReplyOverlayCanceled() {
@@ -1099,8 +1118,7 @@
.split('\n')
.map(line => '> ' + line)
.join('\n') + '\n\n';
- this.$.replyDialog.quote = quoteStr;
- this._openReplyDialog(this.$.replyDialog.FocusTarget.BODY);
+ this._openReplyDialog(FocusTarget.BODY, quoteStr);
}
_handleHideBackgroundContent() {
@@ -1137,9 +1155,9 @@
}
_handleShowReplyDialog(e: CustomEvent<{value: {ccsOnly: boolean}}>) {
- let target = this.$.replyDialog.FocusTarget.REVIEWERS;
+ let target = FocusTarget.REVIEWERS;
if (e.detail.value && e.detail.value.ccsOnly) {
- target = this.$.replyDialog.FocusTarget.CCS;
+ target = FocusTarget.CCS;
}
this._openReplyDialog(target);
}
@@ -1178,6 +1196,24 @@
return this._changeNum !== this.params?.changeNum;
}
+ hasPatchRangeChanged(value: AppElementChangeViewParams) {
+ if (!this._patchRange) return false;
+ if (this._patchRange.basePatchNum !== value.basePatchNum) return true;
+ return this.hasPatchNumChanged(value);
+ }
+
+ hasPatchNumChanged(value: AppElementChangeViewParams) {
+ if (!this._patchRange) return false;
+ if (value.patchNum !== undefined) {
+ return this._patchRange.patchNum !== value.patchNum;
+ } else {
+ // value.patchNum === undefined specifies the latest patchset
+ return (
+ this._patchRange.patchNum !== computeLatestPatchNum(this._allPatchSets)
+ );
+ }
+ }
+
_paramsChanged(value: AppElementChangeViewParams) {
if (value.view !== GerritView.CHANGE) {
this._initialLoadComplete = false;
@@ -1201,43 +1237,53 @@
if (value.basePatchNum === undefined)
value.basePatchNum = ParentPatchSetNum;
- const patchChanged =
- this._patchRange &&
- value.patchNum !== undefined &&
- (this._patchRange.patchNum !== value.patchNum ||
- this._patchRange.basePatchNum !== value.basePatchNum);
+ const patchChanged = this.hasPatchRangeChanged(value);
+ let patchNumChanged = this.hasPatchNumChanged(value);
- let rightPatchNumChanged =
- this._patchRange &&
- value.patchNum !== undefined &&
- this._patchRange.patchNum !== value.patchNum;
-
- const patchRange: ChangeViewPatchRange = {
+ this._patchRange = {
patchNum: value.patchNum,
basePatchNum: value.basePatchNum,
};
-
- this.$.fileList.collapseAllDiffs();
- this._patchRange = patchRange;
this.scrollCommentId = value.commentId;
const patchKnown =
- !patchRange.patchNum ||
- (this._allPatchSets ?? []).some(ps => ps.num === patchRange.patchNum);
+ !this._patchRange.patchNum ||
+ (this._allPatchSets ?? []).some(
+ ps => ps.num === this._patchRange!.patchNum
+ );
+ // _allPatchsets does not know value.patchNum so force a reload.
+ const forceReload = value.forceReload || !patchKnown;
- // If the change has already been loaded and the parameter change is only
- // in the patch range, then don't do a full reload.
- if (this._changeNum !== undefined && patchChanged && patchKnown) {
- if (!patchRange.patchNum) {
- patchRange.patchNum = computeLatestPatchNum(this._allPatchSets);
- rightPatchNumChanged = true;
+ // If changeNum is defined that means the change has already been
+ // rendered once before so a full reload is not required.
+ if (this._changeNum !== undefined && !forceReload) {
+ if (!this._patchRange.patchNum) {
+ this._patchRange = {
+ ...this._patchRange,
+ patchNum: computeLatestPatchNum(this._allPatchSets),
+ };
+ patchNumChanged = true;
}
- this._reloadPatchNumDependentResources(rightPatchNumChanged).then(() => {
- this._sendShowChangeEvent();
- });
+ if (patchChanged) {
+ // We need to collapse all diffs when params change so that a non
+ // existing diff is not requested. See Issue 125270 for more details.
+ this.$.fileList.collapseAllDiffs();
+ this._reloadPatchNumDependentResources(patchNumChanged).then(() => {
+ this._sendShowChangeEvent();
+ });
+ }
+
+ // If there is no change in patchset or changeNum, such as when user goes
+ // to the diff view and then comes back to change page then there is no
+ // need to reload anything and we render the change view component as is.
+ document.documentElement.scrollTop = this.scrollPosition ?? 0;
return;
}
+ // We need to collapse all diffs when params change so that a non existing
+ // diff is not requested. See Issue 125270 for more details.
+ this.$.fileList.collapseAllDiffs();
+
this._initialLoadComplete = false;
this._changeNum = value.changeNum;
this.loadData(true).then(() => {
@@ -1253,8 +1299,8 @@
_initActiveTabs(params?: AppElementChangeViewParams) {
let primaryTab = PrimaryTab.FILES;
- if (params && params.queryMap && params.queryMap.has('tab')) {
- primaryTab = params.queryMap.get('tab') as PrimaryTab;
+ if (params?.tab) {
+ primaryTab = params?.tab as PrimaryTab;
} else if (params && 'commentId' in params) {
primaryTab = PrimaryTab.COMMENT_THREADS;
}
@@ -1390,7 +1436,7 @@
}
if (this.viewState.showReplyDialog) {
- this._openReplyDialog(this.$.replyDialog.FocusTarget.ANY);
+ this._openReplyDialog(FocusTarget.ANY);
this.set('viewState.showReplyDialog', false);
}
});
@@ -1496,7 +1542,7 @@
fireEvent(this, 'show-auth-required');
return;
}
- this._openReplyDialog(this.$.replyDialog.FocusTarget.ANY);
+ this._openReplyDialog(FocusTarget.ANY);
});
}
@@ -1681,12 +1727,17 @@
});
}
- _openReplyDialog(section?: FocusTarget) {
+ _openReplyDialog(focusTarget?: FocusTarget, quote?: string) {
if (!this._change) return;
- this.$.replyOverlay.open().finally(() => {
+ const overlay = this.$.replyOverlay;
+ overlay.open().finally(async () => {
// the following code should be executed no matter open succeed or not
+ const dialog = query<GrReplyDialog>(this, '#replyDialog');
+ assertIsDefined(dialog, 'reply dialog');
this._resetReplyOverlayFocusStops();
- this.$.replyDialog.open(section);
+ dialog.open(focusTarget, quote);
+ const observer = new ResizeObserver(() => overlay.center());
+ observer.observe(dialog);
});
fireDialogChange(this, {opened: true});
this._changeViewAriaHidden = true;
@@ -2017,8 +2068,8 @@
*
* @param isLocationChange Reloads the related changes
* when true and ends reporting events that started on location change.
- * @param clearPatchset Reloads the related changes
- * ignoring any patchset choice made.
+ * @param clearPatchset Reloads the change ignoring any patchset
+ * choice made.
* @return A promise that resolves when the core data has loaded.
* Some non-core data loading may still be in-flight when the core data
* promise resolves.
@@ -2026,7 +2077,14 @@
loadData(isLocationChange?: boolean, clearPatchset?: boolean): Promise<void> {
if (this.isChangeObsolete()) return Promise.resolve();
if (clearPatchset && this._change) {
- GerritNav.navigateToChange(this._change);
+ GerritNav.navigateToChange(
+ this._change,
+ undefined,
+ undefined,
+ undefined,
+ undefined,
+ true
+ );
return Promise.resolve();
}
this._loading = true;
@@ -2168,11 +2226,11 @@
* Kicks off requests for resources that rely on the patch range
* (`this._patchRange`) being defined.
*/
- _reloadPatchNumDependentResources(rightPatchNumChanged?: boolean) {
+ _reloadPatchNumDependentResources(patchNumChanged?: boolean) {
assertIsDefined(this._changeNum, '_changeNum');
if (!this._patchRange?.patchNum) throw new Error('missing patchNum');
const promises = [this._getCommitInfo(), this.$.fileList.reload()];
- if (rightPatchNumChanged)
+ if (patchNumChanged)
promises.push(
this.$.commentAPI.reloadPortedComments(
this._changeNum,
@@ -2422,8 +2480,8 @@
}
@observe('_patchRange.patchNum')
- _patchNumChanged(patchNumStr: PatchSetNum) {
- if (!this._selectedRevision) {
+ _patchNumChanged(patchNumStr?: PatchSetNum) {
+ if (!this._selectedRevision || !patchNumStr) {
return;
}
assertIsDefined(this._change, '_change');
@@ -2470,21 +2528,37 @@
) {
patchNum = this._patchRange.patchNum;
}
- GerritNav.navigateToChange(this._change, patchNum, undefined, true);
+ GerritNav.navigateToChange(
+ this._change,
+ patchNum,
+ undefined,
+ true,
+ undefined,
+ true
+ );
}
_handleStopEditTap() {
assertIsDefined(this._change, '_change');
if (!this._patchRange)
throw new Error('missing required _patchRange property');
- GerritNav.navigateToChange(this._change, this._patchRange.patchNum);
+ GerritNav.navigateToChange(
+ this._change,
+ this._patchRange.patchNum,
+ undefined,
+ undefined,
+ undefined,
+ true
+ );
}
_resetReplyOverlayFocusStops() {
- this.$.replyOverlay.setFocusStops(this.$.replyDialog.getFocusStops());
+ const dialog = query<GrReplyDialog>(this, '#replyDialog');
+ if (!dialog) return;
+ this.$.replyOverlay.setFocusStops(dialog.getFocusStops());
}
- _handleToggleStar(e: CustomEvent<{change: ChangeInfo; starred: boolean}>) {
+ _handleToggleStar(e: CustomEvent<ChangeStarToggleStarDetail>) {
if (e.detail.starred) {
this.reporting.reportInteraction('change-starred-from-change-view');
this.lastStarredTimestamp = Date.now();
@@ -2562,6 +2636,9 @@
}
declare global {
+ interface HTMLElementEventMap {
+ 'toggle-star': CustomEvent<ChangeStarToggleStarDetail>;
+ }
interface HTMLElementTagNameMap {
'gr-change-view': GrChangeView;
}
diff --git a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_html.ts b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_html.ts
index d42fc17..9341b18 100644
--- a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_html.ts
+++ b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_html.ts
@@ -20,6 +20,9 @@
<style include="gr-a11y-styles">
/* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
</style>
+ <style include="gr-paper-styles">
+ /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
+ </style>
<style include="shared-styles">
.container:not(.loading) {
background-color: var(--background-color-tertiary);
@@ -335,7 +338,7 @@
</div>
<gr-change-star
id="changeStar"
- change="{{_change}}"
+ change="[[_change]]"
on-toggle-star="_handleToggleStar"
hidden$="[[!_loggedIn]]"
></gr-change-star>
@@ -695,24 +698,27 @@
no-cancel-on-esc-key=""
scroll-action="lock"
with-backdrop=""
+ opened="{{replyOverlayOpened}}"
on-iron-overlay-canceled="onReplyOverlayCanceled"
>
- <gr-reply-dialog
- id="replyDialog"
- change="{{_change}}"
- patch-num="[[_computeLatestPatchNum(_allPatchSets)]]"
- permitted-labels="[[_change.permitted_labels]]"
- draft-comment-threads="[[_draftCommentThreads]]"
- project-config="[[_projectConfig]]"
- server-config="[[_serverConfig]]"
- can-be-started="[[_canStartReview]]"
- on-send="_handleReplySent"
- on-cancel="_handleReplyCancel"
- on-autogrow="_handleReplyAutogrow"
- on-send-disabled-changed="_resetReplyOverlayFocusStops"
- hidden$="[[!_loggedIn]]"
- >
- </gr-reply-dialog>
+ <template is="dom-if" if="[[replyOverlayOpened]]">
+ <gr-reply-dialog
+ id="replyDialog"
+ change="{{_change}}"
+ patch-num="[[_computeLatestPatchNum(_allPatchSets)]]"
+ permitted-labels="[[_change.permitted_labels]]"
+ draft-comment-threads="[[_draftCommentThreads]]"
+ project-config="[[_projectConfig]]"
+ server-config="[[_serverConfig]]"
+ can-be-started="[[_canStartReview]]"
+ on-send="_handleReplySent"
+ on-cancel="_handleReplyCancel"
+ on-autogrow="_handleReplyAutogrow"
+ on-send-disabled-changed="_resetReplyOverlayFocusStops"
+ hidden$="[[!_loggedIn]]"
+ >
+ </gr-reply-dialog>
+ </template>
</gr-overlay>
<gr-comment-api id="commentAPI"></gr-comment-api>
`;
diff --git a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_test.ts b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_test.ts
index 5fcc3b0..ab17f47 100644
--- a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_test.ts
+++ b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_test.ts
@@ -27,6 +27,7 @@
MessageTag,
PrimaryTab,
createDefaultPreferences,
+ createDefaultDiffPrefs,
} from '../../../constants/constants';
import {GrEditConstants} from '../../edit/gr-edit-constants';
import {_testOnly_resetEndpoints} from '../../shared/gr-js-api-interface/gr-plugin-endpoints';
@@ -36,7 +37,14 @@
import {EventType, PluginApi} from '../../../api/plugin';
import 'lodash/lodash';
-import {mockPromise, stubRestApi, stubUsers} from '../../../test/test-utils';
+import {
+ mockPromise,
+ queryAndAssert,
+ stubRestApi,
+ stubUsers,
+ waitQueryAndAssert,
+ waitUntil,
+} from '../../../test/test-utils';
import {
createAppElementChangeViewParams,
createApproval,
@@ -96,6 +104,9 @@
import {appContext} from '../../../services/app-context';
import {ChangeStates} from '../../shared/gr-change-status/gr-change-status';
import {_testOnly_setState} from '../../../services/user/user-model';
+import {FocusTarget, GrReplyDialog} from '../gr-reply-dialog/gr-reply-dialog';
+import {GrOverlay} from '../../shared/gr-overlay/gr-overlay';
+import {GrChangeStar} from '../../shared/gr-change-star/gr-change-star';
const pluginApi = _testOnly_initGerritPluginApi();
const fixture = fixtureFromElement('gr-change-view');
@@ -547,13 +558,12 @@
test('param change should switch primary tab correctly', async () => {
assert.equal(element._activeTabs[0], PrimaryTab.FILES);
- const queryMap = new Map<string, string>();
- queryMap.set('tab', PrimaryTab.FINDINGS);
// view is required
+ element._changeNum = undefined;
element.params = {
...createAppElementChangeViewParams(),
...element.params,
- queryMap,
+ tab: PrimaryTab.FINDINGS,
};
await flush();
assert.equal(element._activeTabs[0], PrimaryTab.FINDINGS);
@@ -561,13 +571,11 @@
test('invalid param change should not switch primary tab', async () => {
assert.equal(element._activeTabs[0], PrimaryTab.FILES);
- const queryMap = new Map<string, string>();
- queryMap.set('tab', 'random');
// view is required
element.params = {
...createAppElementChangeViewParams(),
...element.params,
- queryMap,
+ tab: 'random',
};
await flush();
assert.equal(element._activeTabs[0], PrimaryTab.FILES);
@@ -680,9 +688,7 @@
element.$.replyOverlay.close();
assert.isFalse(element.$.replyOverlay.opened);
assert(
- openSpy.lastCall.calledWithExactly(
- element.$.replyDialog.FocusTarget.ANY
- ),
+ openSpy.lastCall.calledWithExactly(FocusTarget.ANY),
'_openReplyDialog should have been passed ANY'
);
assert.equal(openSpy.callCount, 1);
@@ -704,7 +710,8 @@
},
};
const handlerSpy = sinon.spy(element, '_handleHideBackgroundContent');
- element.$.replyDialog.dispatchEvent(
+ const overlay = queryAndAssert<GrOverlay>(element, '#replyOverlay');
+ overlay.dispatchEvent(
new CustomEvent('fullscreen-overlay-opened', {
composed: true,
bubbles: true,
@@ -731,7 +738,8 @@
},
};
const handlerSpy = sinon.spy(element, '_handleShowBackgroundContent');
- element.$.replyDialog.dispatchEvent(
+ const overlay = queryAndAssert<GrOverlay>(element, '#replyOverlay');
+ overlay.dispatchEvent(
new CustomEvent('fullscreen-overlay-closed', {
composed: true,
bubbles: true,
@@ -813,7 +821,10 @@
...createDefaultPreferences(),
diff_view: DiffViewMode.SIDE_BY_SIDE,
};
- _testOnly_setState({preferences: prefs});
+ _testOnly_setState({
+ preferences: prefs,
+ diffPreferences: createDefaultDiffPrefs(),
+ });
element._handleToggleDiffMode();
assert.isTrue(
updatePreferencesStub.calledWith({diff_view: DiffViewMode.UNIFIED})
@@ -823,7 +834,10 @@
...createDefaultPreferences(),
diff_view: DiffViewMode.UNIFIED,
};
- _testOnly_setState({preferences: newPrefs});
+ _testOnly_setState({
+ preferences: newPrefs,
+ diffPreferences: createDefaultDiffPrefs(),
+ });
await flush();
element._handleToggleDiffMode();
assert.isTrue(
@@ -1299,12 +1313,12 @@
.callsFake(() => Promise.resolve([undefined, undefined, undefined]));
flush();
const collapseStub = sinon.stub(element.$.fileList, 'collapseAllDiffs');
-
const value: AppElementChangeViewParams = {
...createAppElementChangeViewParams(),
view: GerritView.CHANGE,
patchNum: 1 as RevisionPatchSetNum,
};
+ element._changeNum = undefined;
element.params = value;
await flush();
assert.isTrue(reloadStub.calledOnce);
@@ -1362,7 +1376,7 @@
assert.isTrue(reloadPortedCommentsStub.calledOnce);
});
- test('reload entire page when patchRange doesnt change', async () => {
+ test('do not reload entire page when patchRange doesnt change', async () => {
const reloadStub = sinon
.stub(element, 'loadData')
.callsFake(() => Promise.resolve());
@@ -1370,13 +1384,15 @@
const value: AppElementChangeViewParams =
createAppElementChangeViewParams();
element.params = value;
+ // change already loaded
+ assert.isOk(element._changeNum);
await flush();
- assert.isTrue(reloadStub.calledOnce);
+ assert.isFalse(reloadStub.calledOnce);
element._initialLoadComplete = true;
element.params = {...value};
await flush();
- assert.isTrue(reloadStub.calledTwice);
- assert.isTrue(collapseStub.calledTwice);
+ assert.isFalse(reloadStub.calledTwice);
+ assert.isFalse(collapseStub.calledTwice);
});
test('do not handle new change numbers', async () => {
@@ -1601,9 +1617,7 @@
const openStub = sinon.stub(element, '_openReplyDialog');
tap(element.$.replyBtn);
assert(
- openStub.lastCall.calledWithExactly(
- element.$.replyDialog.FocusTarget.ANY
- ),
+ openStub.lastCall.calledWithExactly(FocusTarget.ANY),
'_openReplyDialog should have been passed ANY'
);
assert.equal(openStub.callCount, 1);
@@ -1622,18 +1636,12 @@
bubbles: true,
})
);
- assert(
- openStub.lastCall.calledWithExactly(
- element.$.replyDialog.FocusTarget.BODY
- ),
- '_openReplyDialog should have been passed BODY'
- );
- assert.equal(openStub.callCount, 1);
+ assert.isTrue(openStub.calledOnce);
+ assert.equal(openStub.lastCall.args[0], FocusTarget.BODY);
}
);
test('reply dialog focus can be controlled', () => {
- const FocusTarget = element.$.replyDialog.FocusTarget;
const openStub = sinon.stub(element, '_openReplyDialog');
const e = new CustomEvent('show-reply-dialog', {
@@ -1709,7 +1717,6 @@
suite('reply dialog tests', () => {
setup(() => {
- sinon.stub(element.$.replyDialog, '_draftChanged');
element._change = {
...createChangeViewChange(),
revisions: createRevisions(1),
@@ -1740,52 +1747,18 @@
assert.isTrue(openReplyDialogStub.calledOnce);
});
- test('reply from comment adds quote text', () => {
+ test('reply from comment adds quote text', async () => {
const e = new CustomEvent('', {
detail: {message: {message: 'quote text'}},
});
element._handleMessageReply(e);
- assert.equal(element.$.replyDialog.quote, '> quote text\n\n');
- });
-
- test('reply from comment replaces quote text', () => {
- element.$.replyDialog.draft = '> old quote text\n\n some draft text';
- element.$.replyDialog.quote = '> old quote text\n\n';
- const e = new CustomEvent('', {
- detail: {message: {message: 'quote text'}},
- });
- element._handleMessageReply(e);
- assert.equal(element.$.replyDialog.quote, '> quote text\n\n');
- });
-
- test('reply from same comment preserves quote text', () => {
- element.$.replyDialog.draft = '> quote text\n\n some draft text';
- element.$.replyDialog.quote = '> quote text\n\n';
- const e = new CustomEvent('', {
- detail: {message: {message: 'quote text'}},
- });
- element._handleMessageReply(e);
- assert.equal(
- element.$.replyDialog.draft,
- '> quote text\n\n some draft text'
+ const dialog = await waitQueryAndAssert<GrReplyDialog>(
+ element,
+ '#replyDialog'
);
- assert.equal(element.$.replyDialog.quote, '> quote text\n\n');
- });
-
- test('reply from top of page contains previous draft', () => {
- const div = document.createElement('div');
- element.$.replyDialog.draft = '> quote text\n\n some draft text';
- element.$.replyDialog.quote = '> quote text\n\n';
- const e = {
- target: div,
- preventDefault: sinon.spy(),
- } as unknown as MouseEvent;
- element._handleReplyTap(e);
- assert.equal(
- element.$.replyDialog.draft,
- '> quote text\n\n some draft text'
- );
- assert.equal(element.$.replyDialog.quote, '> quote text\n\n');
+ const openSpy = sinon.spy(dialog, 'open');
+ await waitUntil(() => openSpy.called && !!openSpy.lastCall.args[1]);
+ assert.equal(openSpy.lastCall.args[1], '> quote text\n\n');
});
});
@@ -2055,6 +2028,39 @@
});
});
+ test('patch range changed', () => {
+ element._patchRange = undefined;
+ element._change = createChangeViewChange();
+ element._change!.revisions = createRevisions(4);
+ element._change.current_revision = '1' as CommitId;
+ element._change = {...element._change};
+
+ const params = createAppElementChangeViewParams();
+
+ assert.isFalse(element.hasPatchRangeChanged(params));
+ assert.isFalse(element.hasPatchNumChanged(params));
+
+ params.basePatchNum = ParentPatchSetNum;
+ // undefined means navigate to latest patchset
+ params.patchNum = undefined;
+
+ element._patchRange = {
+ patchNum: 2 as RevisionPatchSetNum,
+ basePatchNum: ParentPatchSetNum,
+ };
+
+ assert.isTrue(element.hasPatchRangeChanged(params));
+ assert.isTrue(element.hasPatchNumChanged(params));
+
+ element._patchRange = {
+ patchNum: 4 as RevisionPatchSetNum,
+ basePatchNum: ParentPatchSetNum,
+ };
+
+ assert.isFalse(element.hasPatchRangeChanged(params));
+ assert.isFalse(element.hasPatchNumChanged(params));
+ });
+
suite('_handleEditTap', () => {
let fireEdit: () => void;
@@ -2090,7 +2096,7 @@
test('no edit exists in revisions, non-latest patchset', async () => {
const promise = mockPromise();
sinon.stub(GerritNav, 'navigateToChange').callsFake((...args) => {
- assert.equal(args.length, 4);
+ assert.equal(args.length, 6);
assert.equal(args[1], 1 as PatchSetNum); // patchNum
assert.equal(args[3], true); // opt_isEdit
promise.resolve();
@@ -2107,7 +2113,7 @@
test('no edit exists in revisions, latest patchset', async () => {
const promise = mockPromise();
sinon.stub(GerritNav, 'navigateToChange').callsFake((...args) => {
- assert.equal(args.length, 4);
+ assert.equal(args.length, 6);
// No patch should be specified when patchNum == latest.
assert.isNotOk(args[1]); // patchNum
assert.equal(args[3], true); // opt_isEdit
@@ -2131,7 +2137,7 @@
navigateToChangeStub.restore();
const promise = mockPromise();
sinon.stub(GerritNav, 'navigateToChange').callsFake((...args) => {
- assert.equal(args.length, 2);
+ assert.equal(args.length, 6);
assert.equal(args[1], 1 as PatchSetNum); // patchNum
promise.resolve();
});
@@ -2196,17 +2202,19 @@
});
});
- test('_handleToggleStar called when star is tapped', () => {
+ test('_handleToggleStar called when star is tapped', async () => {
element._change = {
...createChangeViewChange(),
owner: {_account_id: 1 as AccountId},
starred: false,
};
element._loggedIn = true;
- const stub = sinon.stub(element, '_handleToggleStar');
- flush();
+ await flush();
- tap(element.$.changeStar.shadowRoot!.querySelector('button')!);
+ const stub = sinon.stub(element, '_handleToggleStar');
+
+ const changeStar = queryAndAssert<GrChangeStar>(element, '#changeStar');
+ tap(queryAndAssert<HTMLButtonElement>(changeStar, 'button')!);
assert.isTrue(stub.called);
});
@@ -2249,6 +2257,8 @@
appContext.reportingService,
'changeFullyLoaded'
);
+ // reset so reload is triggered
+ element._changeNum = undefined;
element.params = {
...createAppElementChangeViewParams(),
changeNum: TEST_NUMERIC_CHANGE_ID,
diff --git a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.ts b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.ts
index 5506bc7..95984b8 100644
--- a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.ts
+++ b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.ts
@@ -83,7 +83,10 @@
import {ParsedChangeInfo, PatchSetFile} from '../../../types/types';
import {Timing} from '../../../constants/reporting';
import {RevisionInfo} from '../../shared/revision-info/revision-info';
-import {preferences$} from '../../../services/user/user-model';
+import {
+ diffPreferences$,
+ sizeBarInChangeTable$,
+} from '../../../services/user/user-model';
import {changeComments$} from '../../../services/comments/comments-model';
import {Subject} from 'rxjs';
import {takeUntil} from 'rxjs/operators';
@@ -317,6 +320,8 @@
private readonly restApiService = appContext.restApiService;
+ private readonly userService = appContext.userService;
+
disconnected$ = new Subject();
/** Called in disconnectedCallback. */
@@ -381,6 +386,16 @@
diffViewMode$
.pipe(takeUntil(this.disconnected$))
.subscribe(diffView => (this.diffViewMode = diffView));
+ diffPreferences$
+ .pipe(takeUntil(this.disconnected$))
+ .subscribe(diffPreferences => {
+ this.diffPrefs = diffPreferences;
+ });
+ sizeBarInChangeTable$
+ .pipe(takeUntil(this.disconnected$))
+ .subscribe(sizeBarInChangeTable => {
+ this._showSizeBars = sizeBarInChangeTable;
+ });
getPluginLoader()
.awaitPluginsLoaded()
@@ -479,16 +494,6 @@
})
);
- promises.push(
- this._getDiffPreferences().then(prefs => {
- this.diffPrefs = prefs;
- })
- );
-
- preferences$.pipe(takeUntil(this.disconnected$)).subscribe(prefs => {
- this._showSizeBars = !!prefs?.size_bar_in_change_table;
- });
-
return Promise.all(promises).then(() => {
this._loading = false;
this._detectChromiteButler();
@@ -1645,9 +1650,7 @@
}
_handleReloadingDiffPreference() {
- this._getDiffPreferences().then(prefs => {
- this.diffPrefs = prefs;
- });
+ this.userService.getDiffPreferences();
}
/**
diff --git a/polygerrit-ui/app/elements/change/gr-label-scores/gr-label-scores.ts b/polygerrit-ui/app/elements/change/gr-label-scores/gr-label-scores.ts
index a496be5..962ccef 100644
--- a/polygerrit-ui/app/elements/change/gr-label-scores/gr-label-scores.ts
+++ b/polygerrit-ui/app/elements/change/gr-label-scores/gr-label-scores.ts
@@ -33,9 +33,11 @@
LabelValuesMap,
} from '../gr-label-score-row/gr-label-score-row';
import {appContext} from '../../../services/app-context';
-import {labelCompare} from '../../../utils/label-util';
+import {getTriggerVotes, labelCompare} from '../../../utils/label-util';
import {Execution} from '../../../constants/reporting';
import {ChangeStatus} from '../../../constants/constants';
+import {KnownExperimentId} from '../../../services/flags/flags';
+import {fontStyles} from '../../../styles/gr-font-styles';
@customElement('gr-label-scores')
export class GrLabelScores extends LitElement {
@@ -50,8 +52,11 @@
private readonly reporting = appContext.reportingService;
+ private readonly flagsService = appContext.flagsService;
+
static override get styles() {
return [
+ fontStyles,
css`
.scoresTable {
display: table;
@@ -72,26 +77,74 @@
gr-label-score-row.no-access {
display: none;
}
+ .heading-3 {
+ padding-left: var(--spacing-xl);
+ margin-bottom: var(--spacing-m);
+ margin-top: var(--spacing-l);
+ }
+ .heading-3:first-of-type {
+ margin-top: 0;
+ }
`,
];
}
override render() {
+ if (this.flagsService.isEnabled(KnownExperimentId.SUBMIT_REQUIREMENTS_UI)) {
+ return this.renderNewSubmitRequirements();
+ } else {
+ return this.renderOldSubmitRequirements();
+ }
+ }
+
+ private renderOldSubmitRequirements() {
const labels = this._computeLabels();
+ return html`${this.renderLabels(labels)}${this.renderErrorMessages()}`;
+ }
+
+ private renderNewSubmitRequirements() {
+ return html`${this.renderSubmitReqsLabels()}${this.renderTriggerVotes()}
+ ${this.renderErrorMessages()}`;
+ }
+
+ private renderSubmitReqsLabels() {
+ const triggerVotes = getTriggerVotes(this.change);
+ const labels = this._computeLabels().filter(
+ label => !triggerVotes.includes(label.name)
+ );
+ if (!labels.length) return;
+ return html`<h3 class="heading-3">Submit requirements votes</h3>
+ ${this.renderLabels(labels)}`;
+ }
+
+ private renderTriggerVotes() {
+ const triggerVotes = getTriggerVotes(this.change);
+ const labels = this._computeLabels().filter(label =>
+ triggerVotes.includes(label.name)
+ );
+ if (!labels.length) return;
+ return html`<h3 class="heading-3">Trigger Votes</h3>
+ ${this.renderLabels(labels)}`;
+ }
+
+ private renderLabels(labels: Label[]) {
const labelValues = this._computeColumns();
return html`<div class="scoresTable">
- ${labels.map(
- label => html`<gr-label-score-row
- class="${this.computeLabelAccessClass(label.name)}"
- .label="${label}"
- .name="${label.name}"
- .labels="${this.change?.labels}"
- .permittedLabels="${this.permittedLabels}"
- .labelValues="${labelValues}"
- ></gr-label-score-row>`
- )}
- </div>
- <div
+ ${labels.map(
+ label => html`<gr-label-score-row
+ class="${this.computeLabelAccessClass(label.name)}"
+ .label="${label}"
+ .name="${label.name}"
+ .labels="${this.change?.labels}"
+ .permittedLabels="${this.permittedLabels}"
+ .labelValues="${labelValues}"
+ ></gr-label-score-row>`
+ )}
+ </div>`;
+ }
+
+ private renderErrorMessages() {
+ return html`<div
class="mergedMessage"
?hidden=${this.change?.status !== ChangeStatus.MERGED}
>
diff --git a/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list.ts b/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list.ts
index e16c073..c3acfb0 100644
--- a/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list.ts
+++ b/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list.ts
@@ -18,6 +18,7 @@
import '../../shared/gr-button/gr-button';
import '../../shared/gr-icons/gr-icons';
import '../gr-message/gr-message';
+import '../../../styles/gr-paper-styles';
import '../../../styles/shared-styles';
import {PolymerElement} from '@polymer/polymer/polymer-element';
import {htmlTemplate} from './gr-messages-list_html';
diff --git a/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list_html.ts b/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list_html.ts
index 56fae87..087ee19 100644
--- a/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list_html.ts
+++ b/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list_html.ts
@@ -51,6 +51,9 @@
border-bottom: 1px solid var(--border-color);
}
</style>
+ <style include="gr-paper-styles">
+ /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
+ </style>
<div class="header">
<div id="showAllActivityToggleContainer" class="container">
<template
diff --git a/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-change.ts b/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-change.ts
index e4703df..744db3b 100644
--- a/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-change.ts
+++ b/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-change.ts
@@ -35,6 +35,9 @@
href?: string;
@property()
+ label?: string;
+
+ @property()
showSubmittableCheck = false;
@property()
@@ -110,7 +113,12 @@
const linkClass = this._computeLinkClass(change);
return html`
<div class="changeContainer">
- <a href="${ifDefined(this.href)}" class="${linkClass}"><slot></slot></a>
+ <a
+ href="${ifDefined(this.href)}"
+ aria-label="${ifDefined(this.label)}"
+ class="${linkClass}"
+ ><slot></slot
+ ></a>
${this.showSubmittableCheck
? html`<span
tabindex="-1"
diff --git a/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-changes-list.ts b/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-changes-list.ts
index bce4024..963c009 100644
--- a/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-changes-list.ts
+++ b/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-changes-list.ts
@@ -33,6 +33,7 @@
import {appContext} from '../../../services/app-context';
import {ParsedChangeInfo} from '../../../types/types';
import {GerritNav} from '../../core/gr-navigation/gr-navigation';
+import {truncatePath} from '../../../utils/path-list-util';
import {pluralize} from '../../../utils/string-util';
import {
changeIsOpen,
@@ -146,6 +147,45 @@
this.conflictingChanges.length,
this.cherryPickChanges.length
);
+
+ const sectionRenderers = [
+ this.renderRelationChain,
+ this.renderSubmittedTogether,
+ this.renderSameTopic,
+ this.renderMergeConflicts,
+ this.renderCherryPicks,
+ ];
+
+ let firstNonEmptySectionFound = false;
+ const sections = [];
+ for (const renderer of sectionRenderers) {
+ const section: TemplateResult<1> | undefined = renderer.call(
+ this,
+ !firstNonEmptySectionFound,
+ sectionSize
+ );
+ firstNonEmptySectionFound = firstNonEmptySectionFound || !!section;
+ sections.push(section);
+ }
+
+ return html`<gr-endpoint-decorator name="related-changes-section">
+ <gr-endpoint-param
+ name="change"
+ .value=${this.change}
+ ></gr-endpoint-param>
+ <gr-endpoint-slot name="top"></gr-endpoint-slot>
+ ${sections}
+ <gr-endpoint-slot name="bottom"></gr-endpoint-slot>
+ </gr-endpoint-decorator>`;
+ }
+
+ private renderRelationChain(
+ isFirst: boolean,
+ sectionSize: (section: Section) => number
+ ) {
+ if (this.relatedChanges.length === 0) {
+ return undefined;
+ }
const relatedChangesMarkersPredicate = this.markersPredicateFactory(
this.relatedChanges.length,
this.relatedChanges.findIndex(relatedChange =>
@@ -158,17 +198,11 @@
this.patchNum,
this.relatedChanges
);
- let firstNonEmptySectionFound = false;
- let isFirstNonEmpty =
- !firstNonEmptySectionFound && !!this.relatedChanges.length;
- firstNonEmptySectionFound = firstNonEmptySectionFound || isFirstNonEmpty;
- const relatedChangeSection = html` <section
- id="relatedChanges"
- ?hidden=${!this.relatedChanges.length}
- >
+
+ return html`<section id="relatedChanges">
<gr-related-collapse
title="Relation chain"
- class="${classMap({first: isFirstNonEmpty})}"
+ class="${classMap({first: isFirst})}"
.length=${this.relatedChanges.length}
.numChangesWhenCollapsed=${sectionSize(Section.RELATED_CHANGES)}
>
@@ -200,8 +234,19 @@
)}
</gr-related-collapse>
</section>`;
+ }
+ private renderSubmittedTogether(
+ isFirst: boolean,
+ sectionSize: (section: Section) => number
+ ) {
const submittedTogetherChanges = this.submittedTogether?.changes ?? [];
+ if (
+ !submittedTogetherChanges.length &&
+ !this.submittedTogether?.non_visible_changes
+ ) {
+ return undefined;
+ }
const countNonVisibleChanges =
this.submittedTogether?.non_visible_changes ?? 0;
const submittedTogetherMarkersPredicate = this.markersPredicateFactory(
@@ -211,19 +256,10 @@
),
sectionSize(Section.SUBMITTED_TOGETHER)
);
- isFirstNonEmpty =
- !firstNonEmptySectionFound &&
- (!!submittedTogetherChanges?.length ||
- !!this.submittedTogether?.non_visible_changes);
- firstNonEmptySectionFound = firstNonEmptySectionFound || isFirstNonEmpty;
- const submittedTogetherSection = html`<section
- id="submittedTogether"
- ?hidden=${!submittedTogetherChanges?.length &&
- !this.submittedTogether?.non_visible_changes}
- >
+ return html`<section id="submittedTogether">
<gr-related-collapse
title="Submitted together"
- class="${classMap({first: isFirstNonEmpty})}"
+ class="${classMap({first: isFirst})}"
.length=${submittedTogetherChanges.length}
.numChangesWhenCollapsed=${sectionSize(Section.SUBMITTED_TOGETHER)}
>
@@ -239,14 +275,14 @@
${this.renderMarkers(
submittedTogetherMarkersPredicate(index)
)}<gr-related-change
+ .label="${this.renderChangeTitle(change)}"
.change="${change}"
.href="${GerritNav.getUrlForChangeById(
change._number,
change.project
)}"
.showSubmittableCheck=${true}
- >${change.project}: ${change.branch}:
- ${change.subject}</gr-related-change
+ >${this.renderChangeLine(change)}</gr-related-change
>
</div>`
)}
@@ -255,22 +291,25 @@
(+ ${pluralize(countNonVisibleChanges, 'non-visible change')})
</div>
</section>`;
+ }
+
+ private renderSameTopic(
+ isFirst: boolean,
+ sectionSize: (section: Section) => number
+ ) {
+ if (!this.sameTopicChanges?.length) {
+ return undefined;
+ }
const sameTopicMarkersPredicate = this.markersPredicateFactory(
this.sameTopicChanges.length,
-1,
sectionSize(Section.SAME_TOPIC)
);
- isFirstNonEmpty =
- !firstNonEmptySectionFound && !!this.sameTopicChanges?.length;
- firstNonEmptySectionFound = firstNonEmptySectionFound || isFirstNonEmpty;
- const sameTopicSection = html`<section
- id="sameTopic"
- ?hidden=${!this.sameTopicChanges?.length}
- >
+ return html`<section id="sameTopic">
<gr-related-collapse
title="Same topic"
- class="${classMap({first: isFirstNonEmpty})}"
+ class="${classMap({first: isFirst})}"
.length=${this.sameTopicChanges.length}
.numChangesWhenCollapsed=${sectionSize(Section.SAME_TOPIC)}
>
@@ -287,33 +326,35 @@
sameTopicMarkersPredicate(index)
)}<gr-related-change
.change="${change}"
+ .label="${this.renderChangeTitle(change)}"
.href="${GerritNav.getUrlForChangeById(
change._number,
change.project
)}"
- >${change.project}: ${change.branch}:
- ${change.subject}</gr-related-change
+ >${this.renderChangeLine(change)}</gr-related-change
>
</div>`
)}
</gr-related-collapse>
</section>`;
+ }
+ private renderMergeConflicts(
+ isFirst: boolean,
+ sectionSize: (section: Section) => number
+ ) {
+ if (!this.conflictingChanges?.length) {
+ return undefined;
+ }
const mergeConflictsMarkersPredicate = this.markersPredicateFactory(
this.conflictingChanges.length,
-1,
sectionSize(Section.MERGE_CONFLICTS)
);
- isFirstNonEmpty =
- !firstNonEmptySectionFound && !!this.conflictingChanges?.length;
- firstNonEmptySectionFound = firstNonEmptySectionFound || isFirstNonEmpty;
- const mergeConflictsSection = html`<section
- id="mergeConflicts"
- ?hidden=${!this.conflictingChanges?.length}
- >
+ return html`<section id="mergeConflicts">
<gr-related-collapse
title="Merge conflicts"
- class="${classMap({first: isFirstNonEmpty})}"
+ class="${classMap({first: isFirst})}"
.length=${this.conflictingChanges.length}
.numChangesWhenCollapsed=${sectionSize(Section.MERGE_CONFLICTS)}
>
@@ -340,22 +381,24 @@
)}
</gr-related-collapse>
</section>`;
+ }
+ private renderCherryPicks(
+ isFirst: boolean,
+ sectionSize: (section: Section) => number
+ ) {
+ if (!this.cherryPickChanges.length) {
+ return undefined;
+ }
const cherryPicksMarkersPredicate = this.markersPredicateFactory(
this.cherryPickChanges.length,
-1,
sectionSize(Section.CHERRY_PICKS)
);
- isFirstNonEmpty =
- !firstNonEmptySectionFound && !!this.cherryPickChanges?.length;
- firstNonEmptySectionFound = firstNonEmptySectionFound || isFirstNonEmpty;
- const cherryPicksSection = html`<section
- id="cherryPicks"
- ?hidden=${!this.cherryPickChanges?.length}
- >
+ return html`<section id="cherryPicks">
<gr-related-collapse
title="Cherry picks"
- class="${classMap({first: isFirstNonEmpty})}"
+ class="${classMap({first: isFirst})}"
.length=${this.cherryPickChanges.length}
.numChangesWhenCollapsed=${sectionSize(Section.CHERRY_PICKS)}
>
@@ -382,17 +425,17 @@
)}
</gr-related-collapse>
</section>`;
+ }
- return html`<gr-endpoint-decorator name="related-changes-section">
- <gr-endpoint-param
- name="change"
- .value=${this.change}
- ></gr-endpoint-param>
- <gr-endpoint-slot name="top"></gr-endpoint-slot>
- ${relatedChangeSection} ${submittedTogetherSection} ${sameTopicSection}
- ${mergeConflictsSection} ${cherryPicksSection}
- <gr-endpoint-slot name="bottom"></gr-endpoint-slot>
- </gr-endpoint-decorator>`;
+ private renderChangeTitle(change: ChangeInfo) {
+ return `${change.project}: ${change.branch}: ${change.subject}`;
+ }
+
+ private renderChangeLine(change: ChangeInfo) {
+ const truncatedRepo = truncatePath(change.project, 2);
+ return html`<span class="truncatedRepo" .title="${change.project}"
+ >${truncatedRepo}</span
+ >: ${change.branch}: ${change.subject}`;
}
sectionSizeFactory(
@@ -691,18 +734,13 @@
css`
.title {
color: var(--deemphasized-text-color);
- padding-left: var(--metadata-horizontal-padding);
- }
- h4 {
display: flex;
align-self: flex-end;
+ margin-left: 20px;
}
gr-button {
display: flex;
}
- h4 {
- margin-left: 20px;
- }
gr-button iron-icon {
color: inherit;
--iron-icon-height: 18px;
diff --git a/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-changes-list_test.ts b/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-changes-list_test.ts
index a6dc338f..d0b56fb 100644
--- a/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-changes-list_test.ts
+++ b/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-changes-list_test.ts
@@ -30,6 +30,7 @@
createSubmittedTogetherInfo,
} from '../../../test/test-data-generators';
import {
+ query,
queryAndAssert,
resetPlugins,
stubRestApi,
@@ -227,11 +228,8 @@
Promise.resolve(submittedTogether)
);
await element.reload();
- const relatedChanges = queryAndAssert<GrRelatedCollapse>(
- queryAndAssert<HTMLElement>(element, '#relatedChanges'),
- 'gr-related-collapse'
- );
- assert.isFalse(relatedChanges!.classList.contains('first'));
+ const relatedChanges = query<HTMLElement>(element, '#relatedChanges');
+ assert.notExists(relatedChanges);
const submittedTogetherSection = queryAndAssert<GrRelatedCollapse>(
queryAndAssert<HTMLElement>(element, '#submittedTogether'),
'gr-related-collapse'
@@ -255,11 +253,11 @@
'gr-related-collapse'
);
assert.isTrue(relatedChanges!.classList.contains('first'));
- const submittedTogetherSection = queryAndAssert<GrRelatedCollapse>(
- queryAndAssert<HTMLElement>(element, '#submittedTogether'),
- 'gr-related-collapse'
+ const submittedTogetherSection = query<HTMLElement>(
+ element,
+ '#submittedTogether'
);
- assert.isFalse(submittedTogetherSection!.classList.contains('first'));
+ assert.notExists(submittedTogetherSection);
const cherryPicks = queryAndAssert<GrRelatedCollapse>(
queryAndAssert<HTMLElement>(element, '#cherryPicks'),
'gr-related-collapse'
diff --git a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.ts b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.ts
index b8932e5..5109b72 100644
--- a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.ts
+++ b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.ts
@@ -240,9 +240,6 @@
@property({type: String, observer: '_draftChanged'})
draft = '';
- @property({type: String})
- quote = '';
-
@property({type: Object})
filterReviewerSuggestion: (input: Suggestion) => boolean;
@@ -427,7 +424,13 @@
super.disconnectedCallback();
}
- open(focusTarget?: FocusTarget) {
+ /**
+ * Note that this method is not actually *opening* the dialog. Opening and
+ * showing the dialog is dealt with by the overlay. This method is used by the
+ * change view for initializing the dialog after opening the overlay. Maybe it
+ * should be called `onOpened()` or `initialize()`?
+ */
+ open(focusTarget?: FocusTarget, quote?: string) {
assertIsDefined(this.change, 'change');
this.knownLatestState = LatestPatchState.CHECKING;
this.changeService.fetchChangeUpdates(this.change).then(result => {
@@ -437,10 +440,9 @@
});
this._focusOn(focusTarget);
- if (this.quote && this.quote.length) {
- // If a reply quote has been provided, use it and clear the property.
- this.draft = this.quote;
- this.quote = '';
+ if (quote?.length) {
+ // If a reply quote has been provided, use it.
+ this.draft = quote;
} else {
// Otherwise, check for an unsaved draft in localstorage.
this.draft = this._loadStoredDraft();
diff --git a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_html.ts b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_html.ts
index 45a55c1..4a8b996 100644
--- a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_html.ts
+++ b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_html.ts
@@ -341,7 +341,6 @@
class="message newReplyDialog"
autocomplete="on"
placeholder="[[_messagePlaceholder]]"
- fixed-position-dropdown=""
monospace="true"
disabled="{{disabled}}"
rows="4"
diff --git a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_test.ts b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_test.ts
index 5a11f47..cf31a4f 100644
--- a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_test.ts
+++ b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_test.ts
@@ -18,9 +18,11 @@
import '../../../test/common-test-setup-karma';
import './gr-reply-dialog';
import {
+ addListenerForTest,
mockPromise,
queryAll,
queryAndAssert,
+ stubRestApi,
stubStorage,
} from '../../../test/test-utils';
import {
@@ -29,8 +31,6 @@
SpecialFilePath,
} from '../../../constants/constants';
import {appContext} from '../../../services/app-context';
-import {addListenerForTest} from '../../../test/test-utils';
-import {stubRestApi} from '../../../test/test-utils';
import {JSON_PREFIX} from '../../shared/gr-rest-api-interface/gr-rest-apis/gr-rest-api-helper';
import {StandardLabels} from '../../../utils/label-util';
import {
@@ -44,7 +44,7 @@
pressAndReleaseKeyOn,
tap,
} from '@polymer/iron-test-helpers/mock-interactions';
-import {GrReplyDialog} from './gr-reply-dialog';
+import {FocusTarget, GrReplyDialog} from './gr-reply-dialog';
import {
AccountId,
AccountInfo,
@@ -1317,11 +1317,9 @@
const storedDraft = 'hello world';
const quote = '> foo bar';
getDraftCommentStub.returns({message: storedDraft});
- element.quote = quote;
- element.open();
+ element.open(FocusTarget.ANY, quote);
assert.isFalse(getDraftCommentStub.called);
assert.equal(element.draft, quote);
- assert.isNotOk(element.quote);
});
test('updates stored draft on edits', async () => {
diff --git a/polygerrit-ui/app/elements/change/gr-submit-requirements/gr-submit-requirements.ts b/polygerrit-ui/app/elements/change/gr-submit-requirements/gr-submit-requirements.ts
index 157336d..6406e3c 100644
--- a/polygerrit-ui/app/elements/change/gr-submit-requirements/gr-submit-requirements.ts
+++ b/polygerrit-ui/app/elements/change/gr-submit-requirements/gr-submit-requirements.ts
@@ -29,10 +29,11 @@
SubmitRequirementResultInfo,
SubmitRequirementStatus,
} from '../../../api/rest-api';
-import {unique} from '../../../utils/common-util';
import {
extractAssociatedLabels,
getAllUniqueApprovals,
+ getRequirements,
+ getTriggerVotes,
hasNeutralStatus,
hasVotes,
iconForStatus,
@@ -139,18 +140,9 @@
}
override render() {
- let submit_requirements = orderSubmitRequirements(
- this.change?.submit_requirements ?? []
- ).filter(req => req.status !== SubmitRequirementStatus.NOT_APPLICABLE);
-
- const hasNonLegacyRequirements = submit_requirements.some(
- req => req.is_legacy === false
+ const submit_requirements = orderSubmitRequirements(
+ getRequirements(this.change)
);
- if (hasNonLegacyRequirements) {
- submit_requirements = submit_requirements.filter(
- req => req.is_legacy === false
- );
- }
return html` <h3
class="metadata-title heading-3"
@@ -167,25 +159,8 @@
</tr>
</thead>
<tbody>
- ${submit_requirements.map(
- requirement => html`<tr
- id="requirement-${charsOnly(requirement.name)}"
- >
- <td>${this.renderStatus(requirement.status)}</td>
- <td class="name">
- <gr-limited-text
- class="name"
- limit="25"
- .text="${requirement.name}"
- ></gr-limited-text>
- </td>
- <td>
- <div class="votes-cell">
- ${this.renderVotes(requirement)}
- ${this.renderChecks(requirement)}
- </div>
- </td>
- </tr>`
+ ${submit_requirements.map(requirement =>
+ this.renderRequirement(requirement)
)}
</tbody>
</table>
@@ -200,7 +175,38 @@
></gr-submit-requirement-hovercard>
`
)}
- ${this.renderTriggerVotes(submit_requirements)}`;
+ ${this.renderTriggerVotes()}`;
+ }
+
+ renderRequirement(requirement: SubmitRequirementResultInfo) {
+ return html`
+ <tr id="requirement-${charsOnly(requirement.name)}">
+ <td>${this.renderStatus(requirement.status)}</td>
+ <td class="name">
+ <gr-limited-text
+ class="name"
+ limit="25"
+ .text="${requirement.name}"
+ ></gr-limited-text>
+ </td>
+ <td>
+ <gr-endpoint-decorator
+ class="votes-cell"
+ name="${`submit-requirement-${charsOnly(requirement.name)}`}"
+ >
+ <gr-endpoint-param
+ name="change"
+ .value=${this.change}
+ ></gr-endpoint-param>
+ <gr-endpoint-param
+ name="requirement"
+ .value=${requirement}
+ ></gr-endpoint-param>
+ ${this.renderVotes(requirement)}${this.renderChecks(requirement)}
+ </gr-endpoint-decorator>
+ </td>
+ </tr>
+ `;
}
renderStatus(status: SubmitRequirementStatus) {
@@ -275,15 +281,11 @@
return;
}
- renderTriggerVotes(submitReqs: SubmitRequirementResultInfo[]) {
+ renderTriggerVotes() {
const labels = this.change?.labels ?? {};
- const allLabels = Object.keys(labels);
- const labelAssociatedWithSubmitReqs = submitReqs
- .flatMap(req => extractAssociatedLabels(req))
- .filter(unique);
- const triggerVotes = allLabels
- .filter(label => !labelAssociatedWithSubmitReqs.includes(label))
- .filter(label => hasVotes(labels[label]));
+ const triggerVotes = getTriggerVotes(this.change).filter(label =>
+ hasVotes(labels[label])
+ );
if (!triggerVotes.length) return;
return html`<h3 class="metadata-title heading-3">Trigger Votes</h3>
<section class="trigger-votes">
diff --git a/polygerrit-ui/app/elements/core/gr-navigation/gr-navigation.ts b/polygerrit-ui/app/elements/core/gr-navigation/gr-navigation.ts
index b8f2630..6b4006c 100644
--- a/polygerrit-ui/app/elements/core/gr-navigation/gr-navigation.ts
+++ b/polygerrit-ui/app/elements/core/gr-navigation/gr-navigation.ts
@@ -256,11 +256,9 @@
edit?: boolean;
host?: string;
messageHash?: string;
- queryMap?: Map<string, string> | URLSearchParams;
commentId?: UrlEncodedCommentId;
-
- // TODO(TS): querystring isn't set anywhere, try to remove
- querystring?: string;
+ forceReload?: boolean;
+ tab?: string;
}
export interface GenerateUrlRepoViewParameters {
@@ -612,7 +610,8 @@
patchNum?: PatchSetNum,
basePatchNum?: BasePatchSetNum,
isEdit?: boolean,
- messageHash?: string
+ messageHash?: string,
+ forceReload?: boolean
) {
if (basePatchNum === ParentPatchSetNum) {
basePatchNum = undefined;
@@ -628,6 +627,7 @@
edit: isEdit,
host: change.internalHost || undefined,
messageHash,
+ forceReload,
});
},
@@ -649,17 +649,28 @@
* @param redirect redirect to a change - if true, the current
* location (i.e. page which makes redirect) is not added to a history.
* I.e. back/forward buttons skip current location
- *
+ * @param forceReload Some views are smart about how to handle the reload
+ * of the view. In certain cases we want to force the view to reload
+ * and re-render everything.
*/
+ // TODO(dhruvsri): move the arguments into one options object
navigateToChange(
change: Pick<ChangeInfo, '_number' | 'project' | 'internalHost'>,
patchNum?: PatchSetNum,
basePatchNum?: BasePatchSetNum,
isEdit?: boolean,
- redirect?: boolean
+ redirect?: boolean,
+ forceReload?: boolean
) {
this._navigate(
- this.getUrlForChange(change, patchNum, basePatchNum, isEdit),
+ this.getUrlForChange(
+ change,
+ patchNum,
+ basePatchNum,
+ isEdit,
+ undefined,
+ forceReload
+ ),
redirect
);
},
diff --git a/polygerrit-ui/app/elements/core/gr-router/gr-router.ts b/polygerrit-ui/app/elements/core/gr-router/gr-router.ts
index f0cff85..65ac9df 100644
--- a/polygerrit-ui/app/elements/core/gr-router/gr-router.ts
+++ b/polygerrit-ui/app/elements/core/gr-router/gr-router.ts
@@ -530,17 +530,22 @@
range = '/' + range;
}
let suffix = `${range}`;
- if (params.querystring) {
- suffix += '?' + params.querystring;
- } else if (params.edit) {
- suffix += ',edit';
+ let queryString = '';
+ if (params.forceReload) {
+ queryString = 'forceReload=true';
}
- if (params.messageHash) {
- suffix += params.messageHash;
+ if (params.edit) {
+ suffix += ',edit';
}
if (params.commentId) {
suffix = suffix + `/comments/${params.commentId}`;
}
+ if (queryString) {
+ suffix += '?' + queryString;
+ }
+ if (params.messageHash) {
+ suffix += params.messageHash;
+ }
if (params.project) {
const encodedProject = encodeURL(params.project, true);
return `/c/${encodedProject}/+/${params.changeNum}${suffix}`;
@@ -1563,9 +1568,20 @@
basePatchNum: convertToPatchSetNum(ctx.params[4]) as BasePatchSetNum,
patchNum: convertToPatchSetNum(ctx.params[6]),
view: GerritView.CHANGE,
- queryMap: ctx.queryMap,
};
+ if (ctx.queryMap.has('forceReload')) {
+ params.forceReload = true;
+ history.replaceState(
+ null,
+ '',
+ location.href.replace(/[?&]forceReload=true/, '')
+ );
+ }
+
+ const tab = ctx.queryMap.get('tab');
+ if (tab) params.tab = tab;
+
this.reporting.setRepoName(params.project);
this.reporting.setChangeId(changeNum);
this._redirectOrNavigate(params);
@@ -1661,13 +1677,24 @@
// Parameter order is based on the regex group number matched.
const project = ctx.params[0] as RepoName;
const changeNum = Number(ctx.params[1]) as NumericChangeId;
- this._redirectOrNavigate({
+ const params: GenerateUrlChangeViewParameters = {
project,
changeNum,
patchNum: convertToPatchSetNum(ctx.params[3]),
view: GerritView.CHANGE,
edit: true,
- });
+ tab: ctx.queryMap.get('tab') ?? '',
+ };
+ if (ctx.queryMap.has('forceReload')) {
+ params.forceReload = true;
+ history.replaceState(
+ null,
+ '',
+ location.href.replace(/[?&]forceReload=true/, '')
+ );
+ }
+ this._redirectOrNavigate(params);
+
this.reporting.setRepoName(project);
this.reporting.setChangeId(changeNum);
}
diff --git a/polygerrit-ui/app/elements/core/gr-router/gr-router_test.js b/polygerrit-ui/app/elements/core/gr-router/gr-router_test.js
index b91bf0c..7f1a40b 100644
--- a/polygerrit-ui/app/elements/core/gr-router/gr-router_test.js
+++ b/polygerrit-ui/app/elements/core/gr-router/gr-router_test.js
@@ -312,28 +312,14 @@
changeNum: '1234',
project: 'test',
};
- const paramsWithQuery = {
- view: GerritView.CHANGE,
- changeNum: '1234',
- project: 'test',
- querystring: 'revert&foo=bar',
- };
assert.equal(element._generateUrl(params), '/c/test/+/1234');
- assert.equal(element._generateUrl(paramsWithQuery),
- '/c/test/+/1234?revert&foo=bar');
params.patchNum = 10;
assert.equal(element._generateUrl(params), '/c/test/+/1234/10');
- paramsWithQuery.patchNum = 10;
- assert.equal(element._generateUrl(paramsWithQuery),
- '/c/test/+/1234/10?revert&foo=bar');
params.basePatchNum = 5;
assert.equal(element._generateUrl(params), '/c/test/+/1234/5..10');
- paramsWithQuery.basePatchNum = 5;
- assert.equal(element._generateUrl(paramsWithQuery),
- '/c/test/+/1234/5..10?revert&foo=bar');
params.messageHash = '#123';
assert.equal(element._generateUrl(params), '/c/test/+/1234/5..10#123');
@@ -1382,7 +1368,6 @@
changeNum: 1234,
basePatchNum: 4,
patchNum: 7,
- queryMap: new Map(),
});
assert.isFalse(redirectStub.called);
assert.isTrue(normalizeRangeStub.called);
@@ -1549,6 +1534,7 @@
null,
3, // 3 Patch num
],
+ queryMap: new Map(),
};
const appParams = {
project: 'foo/bar',
@@ -1556,6 +1542,7 @@
view: GerritView.CHANGE,
patchNum: 3,
edit: true,
+ tab: '',
};
element._handleChangeEditRoute(ctx);
diff --git a/polygerrit-ui/app/elements/custom-dark-theme_test.js b/polygerrit-ui/app/elements/custom-dark-theme_test.ts
similarity index 72%
rename from polygerrit-ui/app/elements/custom-dark-theme_test.js
rename to polygerrit-ui/app/elements/custom-dark-theme_test.ts
index 768a461..71e0740 100644
--- a/polygerrit-ui/app/elements/custom-dark-theme_test.js
+++ b/polygerrit-ui/app/elements/custom-dark-theme_test.ts
@@ -14,17 +14,16 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-
-import '../test/common-test-setup-karma.js';
-import {getComputedStyleValue} from '../utils/dom-util.js';
-import './gr-app.js';
-import {getPluginLoader} from './shared/gr-js-api-interface/gr-plugin-loader.js';
-import {removeTheme} from '../styles/themes/dark-theme.js';
+import '../test/common-test-setup-karma';
+import {getComputedStyleValue} from '../utils/dom-util';
+import './gr-app';
+import {getPluginLoader} from './shared/gr-js-api-interface/gr-plugin-loader';
+import {GrApp} from './gr-app';
const basicFixture = fixtureFromElement('gr-app');
suite('gr-app custom dark theme tests', () => {
- let element;
+ let element: GrApp;
setup(async () => {
window.localStorage.setItem('dark-theme', 'true');
@@ -36,7 +35,6 @@
teardown(() => {
window.localStorage.removeItem('dark-theme');
- removeTheme();
// The app sends requests to server. This can lead to
// unexpected gr-alert elements in document.body
document.body.querySelectorAll('gr-alert').forEach(grAlert => {
@@ -45,19 +43,17 @@
});
test('should tried to load dark theme', () => {
- assert.isTrue(
- !!document.head.querySelector('#dark-theme')
- );
+ assert.isTrue(!!document.head.querySelector('#dark-theme'));
});
test('applies the right theme', () => {
assert.equal(
- getComputedStyleValue('--header-background-color', element)
- .toLowerCase(),
- '#3c4043');
+ getComputedStyleValue('--header-background-color', element).toLowerCase(),
+ '#3c4043'
+ );
assert.equal(
- getComputedStyleValue('--footer-background-color', element)
- .toLowerCase(),
- '#3c4043');
+ getComputedStyleValue('--footer-background-color', element).toLowerCase(),
+ '#3c4043'
+ );
});
});
diff --git a/polygerrit-ui/app/elements/custom-light-theme_test.js b/polygerrit-ui/app/elements/custom-light-theme_test.ts
similarity index 69%
rename from polygerrit-ui/app/elements/custom-light-theme_test.js
rename to polygerrit-ui/app/elements/custom-light-theme_test.ts
index c6e9642..80a7cab 100644
--- a/polygerrit-ui/app/elements/custom-light-theme_test.js
+++ b/polygerrit-ui/app/elements/custom-light-theme_test.ts
@@ -14,21 +14,28 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-
-import '../test/common-test-setup-karma.js';
-import {getComputedStyleValue} from '../utils/dom-util.js';
-import './gr-app.js';
-import {getPluginLoader} from './shared/gr-js-api-interface/gr-plugin-loader.js';
-import {stubRestApi} from '../test/test-utils.js';
+import '../test/common-test-setup-karma';
+import {getComputedStyleValue} from '../utils/dom-util';
+import './gr-app';
+import '../styles/themes/app-theme';
+import {getPluginLoader} from './shared/gr-js-api-interface/gr-plugin-loader';
+import {stubRestApi} from '../test/test-utils';
+import {GrApp} from './gr-app';
+import {
+ createAccountDetailWithId,
+ createServerInfo,
+} from '../test/test-data-generators';
const basicFixture = fixtureFromElement('gr-app');
suite('gr-app custom light theme tests', () => {
- let element;
+ let element: GrApp;
setup(async () => {
window.localStorage.removeItem('dark-theme');
- stubRestApi('getConfig').returns(Promise.resolve({test: 'config'}));
- stubRestApi('getAccount').returns(Promise.resolve({}));
+ stubRestApi('getConfig').returns(Promise.resolve(createServerInfo()));
+ stubRestApi('getAccount').returns(
+ Promise.resolve(createAccountDetailWithId())
+ );
stubRestApi('getDiffComments').returns(Promise.resolve({}));
stubRestApi('getDiffRobotComments').returns(Promise.resolve({}));
stubRestApi('getDiffDrafts').returns(Promise.resolve({}));
@@ -52,12 +59,12 @@
test('applies the right theme', () => {
assert.equal(
- getComputedStyleValue('--header-background-color', element)
- .toLowerCase(),
- '#f1f3f4');
+ getComputedStyleValue('--header-background-color', element).toLowerCase(),
+ '#f1f3f4'
+ );
assert.equal(
- getComputedStyleValue('--footer-background-color', element)
- .toLowerCase(),
- 'transparent');
+ getComputedStyleValue('--footer-background-color', element).toLowerCase(),
+ 'transparent'
+ );
});
});
diff --git a/polygerrit-ui/app/elements/diff/gr-apply-fix-dialog/gr-apply-fix-dialog.ts b/polygerrit-ui/app/elements/diff/gr-apply-fix-dialog/gr-apply-fix-dialog.ts
index f5072b9..a6176e1 100644
--- a/polygerrit-ui/app/elements/diff/gr-apply-fix-dialog/gr-apply-fix-dialog.ts
+++ b/polygerrit-ui/app/elements/diff/gr-apply-fix-dialog/gr-apply-fix-dialog.ts
@@ -41,7 +41,6 @@
import {DiffLayer, ParsedChangeInfo} from '../../../types/types';
import {GrButton} from '../../shared/gr-button/gr-button';
import {TokenHighlightLayer} from '../gr-diff-builder/token-highlight-layer';
-import {KnownExperimentId} from '../../../services/flags/flags';
export interface GrApplyFixDialog {
$: {
@@ -109,10 +108,7 @@
constructor() {
super();
this.restApiService.getPreferences().then(prefs => {
- if (
- !prefs?.disable_token_highlighting &&
- appContext.flagsService.isEnabled(KnownExperimentId.TOKEN_HIGHLIGHTING)
- ) {
+ if (!prefs?.disable_token_highlighting) {
this.layers = [new TokenHighlightLayer(this)];
}
});
diff --git a/polygerrit-ui/app/elements/diff/gr-comment-api/gr-comment-api.ts b/polygerrit-ui/app/elements/diff/gr-comment-api/gr-comment-api.ts
index fc22a58..7e7e507 100644
--- a/polygerrit-ui/app/elements/diff/gr-comment-api/gr-comment-api.ts
+++ b/polygerrit-ui/app/elements/diff/gr-comment-api/gr-comment-api.ts
@@ -70,11 +70,11 @@
* elements of that which uses the gr-comment-api.
*/
constructor(
- comments: PathToCommentsInfoMap | undefined,
- robotComments: {[path: string]: RobotCommentInfo[]} | undefined,
- drafts: {[path: string]: DraftInfo[]} | undefined,
- portedComments: PathToCommentsInfoMap | undefined,
- portedDrafts: PathToCommentsInfoMap | undefined
+ comments?: PathToCommentsInfoMap,
+ robotComments?: {[path: string]: RobotCommentInfo[]},
+ drafts?: {[path: string]: DraftInfo[]},
+ portedComments?: PathToCommentsInfoMap,
+ portedDrafts?: PathToCommentsInfoMap
) {
this._comments = addPath(comments);
this._robotComments = addPath(robotComments);
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-builder/token-highlight-layer.ts b/polygerrit-ui/app/elements/diff/gr-diff-builder/token-highlight-layer.ts
index de7d007..54b2450f 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-builder/token-highlight-layer.ts
+++ b/polygerrit-ui/app/elements/diff/gr-diff-builder/token-highlight-layer.ts
@@ -198,7 +198,6 @@
element,
} = this.findTokenAncestor(e?.target);
if (!newHighlight || newHighlight === this.currentHighlight) return;
- if (this.countOccurrences(newHighlight) <= 1) return;
this.hoveredElement = element;
this.updateTokenTask = debounce(
this.updateTokenTask,
@@ -247,13 +246,6 @@
return this.findTokenAncestor(el.parentElement);
}
- countOccurrences(token: string | undefined) {
- if (!token) return 0;
- const linesLeft = this.tokenToLinesLeft.get(token);
- const linesRight = this.tokenToLinesRight.get(token);
- return (linesLeft?.size ?? 0) + (linesRight?.size ?? 0);
- }
-
private updateTokenHighlight(
newHighlight: string | undefined,
newLineNumber: number,
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-builder/token-highlight-layer_test.ts b/polygerrit-ui/app/elements/diff/gr-diff-builder/token-highlight-layer_test.ts
index 2993d35..a0670b8 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-builder/token-highlight-layer_test.ts
+++ b/polygerrit-ui/app/elements/diff/gr-diff-builder/token-highlight-layer_test.ts
@@ -290,6 +290,34 @@
assert.deepEqual(tokenHighlightingCalls[1].details, undefined);
});
+ test('triggers listener on token with single occurrence', async () => {
+ const clock = sinon.useFakeTimers();
+ const line1 = createLine('a tokenWithSingleOccurence');
+ const line2 = createLine('can be highlighted', 2);
+ annotate(line1);
+ annotate(line2, Side.RIGHT, 2);
+ const tokenNode = queryAndAssert(line1, '.tk-tokenWithSingleOccurence');
+ assert.isTrue(tokenNode.classList.contains('token'));
+ dispatchMouseEvent(
+ 'mouseover',
+ MockInteractions.middleOfNode(tokenNode),
+ tokenNode
+ );
+ assert.equal(tokenHighlightingCalls.length, 0);
+ clock.tick(HOVER_DELAY_MS);
+ assert.equal(tokenHighlightingCalls.length, 1);
+ assert.deepEqual(tokenHighlightingCalls[0].details, {
+ token: 'tokenWithSingleOccurence',
+ side: Side.RIGHT,
+ element: tokenNode,
+ range: {start_line: 1, start_column: 3, end_line: 1, end_column: 26},
+ });
+
+ MockInteractions.click(container);
+ assert.equal(tokenHighlightingCalls.length, 2);
+ assert.deepEqual(tokenHighlightingCalls[1].details, undefined);
+ });
+
test('clicking clears highlight', async () => {
const clock = sinon.useFakeTimers();
const line1 = createLine('two words');
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host.ts b/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host.ts
index a828b9c..025e477 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host.ts
+++ b/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host.ts
@@ -336,9 +336,7 @@
const preferencesPromise = appContext.restApiService.getPreferences();
await getPluginLoader().awaitPluginsLoaded();
const prefs = await preferencesPromise;
- const enableTokenHighlight =
- appContext.flagsService.isEnabled(KnownExperimentId.TOKEN_HIGHLIGHTING) &&
- !prefs?.disable_token_highlighting;
+ const enableTokenHighlight = !prefs?.disable_token_highlighting;
assertIsDefined(this.path, 'path');
this._layers = this.getLayers(this.path, enableTokenHighlight);
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host_test.js b/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host_test.js
index 8901636..6facdca 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host_test.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host_test.js
@@ -1304,7 +1304,7 @@
test('gr-diff-host provides syntax highlighting layer', async () => {
stubRestApi('getDiff').returns(Promise.resolve({content: []}));
await element.reload();
- assert.equal(element.$.diff.layers[0], element.syntaxLayer);
+ assert.equal(element.$.diff.layers[1], element.syntaxLayer);
});
test('rendering normal-sized diff does not disable syntax', () => {
@@ -1358,7 +1358,7 @@
test('gr-diff-host provides syntax highlighting layer', async () => {
stubRestApi('getDiff').returns(Promise.resolve({content: []}));
await element.reload();
- assert.equal(element.$.diff.layers[0], element.syntaxLayer);
+ assert.equal(element.$.diff.layers[1], element.syntaxLayer);
});
test('syntax layer should be disabled', () => {
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-preferences-dialog/gr-diff-preferences-dialog.ts b/polygerrit-ui/app/elements/diff/gr-diff-preferences-dialog/gr-diff-preferences-dialog.ts
index 5a8c55d..9f38655b 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-preferences-dialog/gr-diff-preferences-dialog.ts
+++ b/polygerrit-ui/app/elements/diff/gr-diff-preferences-dialog/gr-diff-preferences-dialog.ts
@@ -87,18 +87,16 @@
});
}
- _handleSaveDiffPreferences() {
+ async _handleSaveDiffPreferences() {
this.diffPrefs = this._editableDiffPrefs;
- this.$.diffPreferences.save().then(() => {
- this.dispatchEvent(
- new CustomEvent('reload-diff-preference', {
- composed: true,
- bubbles: false,
- })
- );
-
- this.$.diffPrefsOverlay.close();
- });
+ await this.$.diffPreferences.save();
+ this.dispatchEvent(
+ new CustomEvent('reload-diff-preference', {
+ composed: true,
+ bubbles: false,
+ })
+ );
+ this.$.diffPrefsOverlay.close();
}
}
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.ts b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.ts
index d892c79..0ffe61f 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.ts
+++ b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.ts
@@ -98,7 +98,7 @@
getPatchRangeForCommentUrl,
isInBaseOfPatchRange,
} from '../../../utils/comment-util';
-import {AppElementParams} from '../../gr-app-types';
+import {AppElementParams, AppElementDiffViewParam} from '../../gr-app-types';
import {EventType, OpenFixPreviewEvent} from '../../../types/events';
import {fireAlert, fireEvent, fireTitleChange} from '../../../utils/event-util';
import {GerritView} from '../../../services/router/router-model';
@@ -109,8 +109,11 @@
import {changeComments$} from '../../../services/comments/comments-model';
import {takeUntil} from 'rxjs/operators';
import {Subject} from 'rxjs';
-import {preferences$} from '../../../services/user/user-model';
import {listen} from '../../../services/shortcuts/shortcuts-service';
+import {
+ preferences$,
+ diffPreferences$,
+} from '../../../services/user/user-model';
const ERR_REVIEW_STATUS = 'Couldn’t change file review status.';
const LOADING_BLAME = 'Loading blame...';
@@ -365,10 +368,6 @@
this._getLoggedIn().then(loggedIn => {
this._loggedIn = loggedIn;
});
- // TODO(brohlfs): This just ensures that the userService is instantiated at
- // all. We need the service to manage the model, but we are not making any
- // direct calls. Will need to find a better solution to this problem ...
- assertIsDefined(appContext.userService);
changeComments$
.pipe(takeUntil(this.disconnected$))
@@ -379,6 +378,11 @@
preferences$.pipe(takeUntil(this.disconnected$)).subscribe(preferences => {
this._userPrefs = preferences;
});
+ diffPreferences$
+ .pipe(takeUntil(this.disconnected$))
+ .subscribe(diffPreferences => {
+ this._prefs = diffPreferences;
+ });
this.addEventListener('open-fix-preview', e => this._onOpenFixPreview(e));
this.cursor.replaceDiffs([this.$.diffHost]);
this._onRenderHandler = (_: Event) => {
@@ -498,12 +502,6 @@
});
}
- _getDiffPreferences() {
- return this.restApiService.getDiffPreferences().then(prefs => {
- this._prefs = prefs;
- });
- }
-
_getPreferences() {
return this.restApiService.getPreferences();
}
@@ -1016,17 +1014,36 @@
);
}
+ private isSameDiffLoaded(value: AppElementDiffViewParam) {
+ return (
+ this._patchRange?.basePatchNum === value.basePatchNum &&
+ this._patchRange?.patchNum === value.patchNum &&
+ this._path === value.path
+ );
+ }
+
_paramsChanged(value: AppElementParams) {
if (value.view !== GerritView.DIFF) {
return;
}
+ // The diff view is kept in the background once created. If the user
+ // scrolls in the change page, the scrolling is reflected in the diff view
+ // as well, which means the diff is scrolled to a random position based
+ // on how much the change view was scrolled.
+ // Hence, reset the scroll position here.
+ document.documentElement.scrollTop = 0;
+
// Everything in the diff view is tied to the change. It seems better to
// force the re-creation of the diff view when the change number changes.
const changeChanged = this._changeNum !== value.changeNum;
if (this._changeNum !== undefined && changeChanged) {
fireEvent(this, EventType.RECREATE_DIFF_VIEW);
return;
+ } else if (this._changeNum !== undefined && this.isSameDiffLoaded(value)) {
+ // changeNum has not changed, so check if there are changes in patchRange
+ // path. If no changes then we can simply render the view as is.
+ return;
}
this._files = {sortedFileList: [], changeFilesByPath: {}};
@@ -1055,8 +1072,6 @@
const promises: Promise<unknown>[] = [];
- promises.push(this._getDiffPreferences());
-
if (!this._change) promises.push(this._getChangeDetail(this._changeNum));
if (!this._changeComments) this._loadComments(value.patchNum);
@@ -1709,7 +1724,7 @@
}
_handleReloadingDiffPreference() {
- this._getDiffPreferences();
+ this.userService.getDiffPreferences();
}
_computeCanEdit(
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view_test.js b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view_test.js
index cc9aac0..46ec5d0 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view_test.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view_test.js
@@ -331,6 +331,7 @@
assert.isFalse(getDiffChangeDetailStub.called);
sinon.stub(element.reporting, 'diffViewDisplayed');
sinon.stub(element, '_loadBlame');
+ sinon.stub(element, '_pathChanged');
sinon.stub(element.$.diffHost, 'reload').returns(Promise.resolve());
sinon.spy(element, '_paramsChanged');
element._change = undefined;
@@ -993,7 +994,9 @@
});
suite('diff prefs hidden', () => {
- test('whenlogged out', () => {
+ test('when no prefs or logged out', () => {
+ element._prefs = undefined;
+ element.disableDiffPrefs = false;
element._loggedIn = false;
flush();
assert.isTrue(element.$.diffPrefsContainer.hidden);
diff --git a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.ts b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.ts
index 7efd2f8..6003a2f 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.ts
+++ b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.ts
@@ -950,7 +950,7 @@
// Safari is not binding newly created comment-thread
// with the slot somehow, replace itself will rebind it
// @see Issue 11182
- if (lastEl && lastEl.replaceWith) {
+ if (isSafari() && lastEl && lastEl.replaceWith) {
lastEl.replaceWith(lastEl);
}
diff --git a/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select.ts b/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select.ts
index 857ffa2..501f688 100644
--- a/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select.ts
+++ b/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select.ts
@@ -14,14 +14,9 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-import '../../../styles/gr-a11y-styles';
-import '../../../styles/shared-styles';
import '../../shared/gr-dropdown-list/gr-dropdown-list';
import '../../shared/gr-select/gr-select';
-import {dom, EventApi} from '@polymer/polymer/lib/legacy/polymer.dom';
-import {PolymerElement} from '@polymer/polymer/polymer-element';
-import {htmlTemplate} from './gr-patch-range-select_html';
-import {pluralize} from '../../../utils/string-util';
+import {convertToString, pluralize} from '../../../utils/string-util';
import {appContext} from '../../../services/app-context';
import {
computeLatestPatchNum,
@@ -33,7 +28,6 @@
PatchSet,
convertToPatchSetNum,
} from '../../../utils/patch-set-util';
-import {customElement, property, observe} from '@polymer/decorators';
import {ReportingService} from '../../../services/gr-reporting/gr-reporting';
import {hasOwnProperty} from '../../../utils/common-util';
import {
@@ -44,7 +38,6 @@
Timestamp,
} from '../../../types/common';
import {RevisionInfo as RevisionInfoClass} from '../../shared/revision-info/revision-info';
-import {PolymerDeepPropertyChange} from '@polymer/polymer/interfaces';
import {ChangeComments} from '../gr-comment-api/gr-comment-api';
import {
DropdownItem,
@@ -52,6 +45,11 @@
GrDropdownList,
} from '../../shared/gr-dropdown-list/gr-dropdown-list';
import {GeneratedWebLink} from '../../core/gr-navigation/gr-navigation';
+import {EditRevisionInfo} from '../../../types/types';
+import {a11yStyles} from '../../../styles/gr-a11y-styles';
+import {sharedStyles} from '../../../styles/shared-styles';
+import {LitElement, PropertyValues, css, html} from 'lit';
+import {customElement, property, query, state} from 'lit/decorators';
// Maximum length for patch set descriptions.
const PATCH_DESC_MAX_LENGTH = 500;
@@ -68,10 +66,13 @@
meta_b: GeneratedWebLink[];
}
-export interface GrPatchRangeSelect {
- $: {
- patchNumDropdown: GrDropdownList;
- };
+declare global {
+ interface HTMLElementEventMap {
+ 'value-change': DropDownValueChangeEvent;
+ }
+ interface HTMLElementTagNameMap {
+ 'gr-patch-range-select': GrPatchRangeSelect;
+ }
}
/**
@@ -83,30 +84,13 @@
* @property {string} basePatchNum
*/
@customElement('gr-patch-range-select')
-export class GrPatchRangeSelect extends PolymerElement {
- static get template() {
- return htmlTemplate;
- }
+export class GrPatchRangeSelect extends LitElement {
+ @query('#patchNumDropdown')
+ patchNumDropdown?: GrDropdownList;
@property({type: Array})
availablePatches?: PatchSet[];
- @property({
- type: Object,
- computed:
- '_computeBaseDropdownContent(availablePatches, patchNum,' +
- '_sortedRevisions, changeComments, revisionInfo)',
- })
- _baseDropdownContent?: DropdownItem[];
-
- @property({
- type: Object,
- computed:
- '_computePatchDropdownContent(availablePatches,' +
- 'basePatchNum, _sortedRevisions, changeComments)',
- })
- _patchDropdownContent?: DropdownItem[];
-
@property({type: String})
changeNum?: string;
@@ -129,13 +113,106 @@
revisionInfo?: RevisionInfoClass;
@property({type: Array})
- _sortedRevisions?: RevisionInfo[];
+ @state()
+ protected sortedRevisions?: RevisionInfo[];
private readonly reporting: ReportingService = appContext.reportingService;
- constructor() {
- super();
- this.reporting = appContext.reportingService;
+ static override get styles() {
+ return [
+ a11yStyles,
+ sharedStyles,
+ css`
+ :host {
+ align-items: center;
+ display: flex;
+ }
+ select {
+ max-width: 15em;
+ }
+ .arrow {
+ color: var(--deemphasized-text-color);
+ margin: 0 var(--spacing-m);
+ }
+ gr-dropdown-list {
+ --trigger-style-text-color: var(--deemphasized-text-color);
+ --trigger-style-font-family: var(--font-family);
+ }
+ @media screen and (max-width: 50em) {
+ .filesWeblinks {
+ display: none;
+ }
+ gr-dropdown-list {
+ --native-select-style: {
+ max-width: 5.25em;
+ }
+ }
+ }
+ `,
+ ];
+ }
+
+ private renderWeblinks(fileLink?: GeneratedWebLink[]) {
+ if (!fileLink) return;
+
+ return html`<span class="filesWeblinks">
+ ${fileLink.map(
+ weblink => html`
+ <a target="_blank" rel="noopener" href="${weblink.url}">
+ ${weblink.name}
+ </a>
+ `
+ )}</span
+ > `;
+ }
+
+ override render() {
+ return html`
+ <h3 class="assistive-tech-only">Patchset Range Selection</h3>
+ <span class="patchRange" aria-label="patch range starts with">
+ <gr-dropdown-list
+ id="basePatchDropdown"
+ .value="${convertToString(this.basePatchNum)}"
+ .items="${this._computeBaseDropdownContent(
+ this.availablePatches,
+ this.patchNum,
+ this.sortedRevisions,
+ this.changeComments,
+ this.revisionInfo
+ )}"
+ @value-change=${this._handlePatchChange}
+ >
+ </gr-dropdown-list>
+ </span>
+ ${this.renderWeblinks(this.filesWeblinks?.meta_a)}
+ <span aria-hidden="true" class="arrow">→</span>
+ <span class="patchRange" aria-label="patch range ends with">
+ <gr-dropdown-list
+ id="patchNumDropdown"
+ .value="${convertToString(this.patchNum)}"
+ .items="${this._computePatchDropdownContent(
+ this.availablePatches,
+ this.basePatchNum,
+ this.sortedRevisions,
+ this.changeComments
+ )}"
+ @value-change=${this._handlePatchChange}
+ >
+ </gr-dropdown-list>
+ ${this.renderWeblinks(this.filesWeblinks?.meta_b)}
+ </span>
+ `;
+ }
+
+ override updated(changedProperties: PropertyValues) {
+ if (changedProperties.has('revisions')) {
+ this._updateSortedRevisions(this.revisions);
+ }
+ }
+
+ _updateSortedRevisions(revisions?: RevisionInfo[]) {
+ if (!revisions) return;
+ this.sortedRevisions = sortRevisions(Object.values(revisions));
}
_getShaForPatch(patch: PatchSet) {
@@ -145,19 +222,19 @@
_computeBaseDropdownContent(
availablePatches?: PatchSet[],
patchNum?: PatchSetNum,
- _sortedRevisions?: RevisionInfo[],
+ sortedRevisions?: (RevisionInfo | EditRevisionInfo)[],
changeComments?: ChangeComments,
revisionInfo?: RevisionInfoClass
- ): DropdownItem[] | undefined {
+ ): DropdownItem[] {
// Polymer 2: check for undefined
if (
availablePatches === undefined ||
patchNum === undefined ||
- _sortedRevisions === undefined ||
+ sortedRevisions === undefined ||
changeComments === undefined ||
revisionInfo === undefined
) {
- return undefined;
+ return [];
}
const parentCounts = revisionInfo.getParentCountMap();
@@ -173,7 +250,7 @@
const entry: DropdownItem = this._createDropdownEntry(
basePatchNum,
'Patchset ',
- _sortedRevisions,
+ sortedRevisions,
changeComments,
this._getShaForPatch(basePatch)
);
@@ -182,7 +259,7 @@
disabled: this._computeLeftDisabled(
basePatch.num,
patchNum,
- _sortedRevisions
+ sortedRevisions
),
});
}
@@ -208,7 +285,7 @@
_computeMobileText(
patchNum: PatchSetNum,
changeComments: ChangeComments,
- revisions: RevisionInfo[]
+ revisions: (RevisionInfo | EditRevisionInfo)[]
) {
return (
`${patchNum}` +
@@ -220,17 +297,17 @@
_computePatchDropdownContent(
availablePatches?: PatchSet[],
basePatchNum?: BasePatchSetNum,
- _sortedRevisions?: RevisionInfo[],
+ sortedRevisions?: (RevisionInfo | EditRevisionInfo)[],
changeComments?: ChangeComments
- ): DropdownItem[] | undefined {
+ ): DropdownItem[] {
// Polymer 2: check for undefined
if (
availablePatches === undefined ||
basePatchNum === undefined ||
- _sortedRevisions === undefined ||
+ sortedRevisions === undefined ||
changeComments === undefined
) {
- return undefined;
+ return [];
}
const dropdownContent: DropdownItem[] = [];
@@ -239,7 +316,7 @@
const entry = this._createDropdownEntry(
patchNum,
patchNum === 'edit' ? '' : 'Patchset ',
- _sortedRevisions,
+ sortedRevisions,
changeComments,
this._getShaForPatch(patch)
);
@@ -248,7 +325,7 @@
disabled: this._computeRightDisabled(
basePatchNum,
patchNum,
- _sortedRevisions
+ sortedRevisions
),
});
}
@@ -271,7 +348,7 @@
_createDropdownEntry(
patchNum: PatchSetNum,
prefix: string,
- sortedRevisions: RevisionInfo[],
+ sortedRevisions: (RevisionInfo | EditRevisionInfo)[],
changeComments: ChangeComments,
sha: string
) {
@@ -296,15 +373,6 @@
return entry;
}
- @observe('revisions.*')
- _updateSortedRevisions(
- revisionsRecord: PolymerDeepPropertyChange<RevisionInfo[], RevisionInfo[]>
- ) {
- const revisions = revisionsRecord.base;
- if (!revisions) return;
- this._sortedRevisions = sortRevisions(Object.values(revisions));
- }
-
/**
* The basePatchNum should always be <= patchNum -- because sortedRevisions
* is sorted in reverse order (higher patchset nums first), invalid base
@@ -316,7 +384,7 @@
_computeLeftDisabled(
basePatchNum: PatchSetNum,
patchNum: PatchSetNum,
- sortedRevisions: RevisionInfo[]
+ sortedRevisions: (RevisionInfo | EditRevisionInfo)[]
): boolean {
return (
findSortedIndex(basePatchNum, sortedRevisions) <=
@@ -341,7 +409,7 @@
_computeRightDisabled(
basePatchNum: PatchSetNum,
patchNum: PatchSetNum,
- sortedRevisions: RevisionInfo[]
+ sortedRevisions: (RevisionInfo | EditRevisionInfo)[]
): boolean {
if (basePatchNum === ParentPatchSetNum) {
return false;
@@ -401,7 +469,7 @@
}
_computePatchSetDescription(
- revisions: RevisionInfo[],
+ revisions: (RevisionInfo | EditRevisionInfo)[],
patchNum: PatchSetNum,
addFrontSpace?: boolean
) {
@@ -413,7 +481,7 @@
}
_computePatchSetDate(
- revisions: RevisionInfo[],
+ revisions: (RevisionInfo | EditRevisionInfo)[],
patchNum: PatchSetNum
): Timestamp | undefined {
const rev = getRevisionByPatchNum(revisions, patchNum);
@@ -429,10 +497,10 @@
patchNum: this.patchNum,
basePatchNum: this.basePatchNum,
};
- const target = (dom(e) as EventApi).localTarget;
+ const target = e.target;
const patchSetValue = convertToPatchSetNum(e.detail.value)!;
const latestPatchNum = computeLatestPatchNum(this.availablePatches);
- if (target === this.$.patchNumDropdown) {
+ if (target === this.patchNumDropdown) {
if (detail.patchNum === e.detail.value) return;
this.reporting.reportInteraction('right-patchset-changed', {
previous: detail.patchNum,
@@ -460,9 +528,3 @@
);
}
}
-
-declare global {
- interface HTMLElementTagNameMap {
- 'gr-patch-range-select': GrPatchRangeSelect;
- }
-}
diff --git a/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select_html.ts b/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select_html.ts
deleted file mode 100644
index 26944a4..0000000
--- a/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select_html.ts
+++ /dev/null
@@ -1,82 +0,0 @@
-/**
- * @license
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-import {html} from '@polymer/polymer/lib/utils/html-tag';
-
-export const htmlTemplate = html`
- <style include="gr-a11y-styles">
- /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
- </style>
- <style include="shared-styles">
- :host {
- align-items: center;
- display: flex;
- }
- select {
- max-width: 15em;
- }
- .arrow {
- color: var(--deemphasized-text-color);
- margin: 0 var(--spacing-m);
- }
- gr-dropdown-list {
- --trigger-style-text-color: var(--deemphasized-text-color);
- --trigger-style-font-family: var(--font-family);
- }
- @media screen and (max-width: 50em) {
- .filesWeblinks {
- display: none;
- }
- gr-dropdown-list {
- --native-select-style: {
- max-width: 5.25em;
- }
- }
- }
- </style>
- <h3 class="assistive-tech-only">Patchset Range Selection</h3>
- <span class="patchRange" aria-label="patch range starts with">
- <gr-dropdown-list
- id="basePatchDropdown"
- value="[[basePatchNum]]"
- on-value-change="_handlePatchChange"
- items="[[_baseDropdownContent]]"
- >
- </gr-dropdown-list>
- </span>
- <span is="dom-if" if="[[filesWeblinks.meta_a]]" class="filesWeblinks">
- <template is="dom-repeat" items="[[filesWeblinks.meta_a]]" as="weblink">
- <a target="_blank" rel="noopener" href$="[[weblink.url]]"
- >[[weblink.name]]</a
- >
- </template>
- </span>
- <span aria-hidden="true" class="arrow">→</span>
- <span class="patchRange" aria-label="patch range ends with">
- <gr-dropdown-list
- id="patchNumDropdown"
- value="[[patchNum]]"
- on-value-change="_handlePatchChange"
- items="[[_patchDropdownContent]]"
- >
- </gr-dropdown-list>
- <span is="dom-if" if="[[filesWeblinks.meta_b]]" class="filesWeblinks">
- <template is="dom-repeat" items="[[filesWeblinks.meta_b]]" as="weblink">
- <a target="_blank" href$="[[weblink.url]]">[[weblink.name]]</a>
- </template>
- </span>
- </span>
-`;
diff --git a/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select_test.js b/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select_test.js
deleted file mode 100644
index 28ebbac..0000000
--- a/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select_test.js
+++ /dev/null
@@ -1,395 +0,0 @@
-/**
- * @license
- * Copyright (C) 2015 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-import '../../../test/common-test-setup-karma.js';
-import '../gr-comment-api/gr-comment-api.js';
-import '../../shared/revision-info/revision-info.js';
-import './gr-patch-range-select.js';
-import '../../../test/mocks/comment-api.js';
-import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js';
-import {RevisionInfo} from '../../shared/revision-info/revision-info.js';
-import {createCommentApiMockWithTemplateElement} from '../../../test/mocks/comment-api';
-import {html} from '@polymer/polymer/lib/utils/html-tag.js';
-import {ChangeComments} from '../gr-comment-api/gr-comment-api.js';
-import {stubRestApi} from '../../../test/test-utils.js';
-import {EditPatchSetNum} from '../../../types/common.js';
-import {SpecialFilePath} from '../../../constants/constants.js';
-
-const commentApiMockElement = createCommentApiMockWithTemplateElement(
- 'gr-patch-range-select-comment-api-mock', html`
- <gr-patch-range-select id="patchRange" auto
- change-comments="[[_changeComments]]"></gr-patch-range-select>
- <gr-comment-api id="commentAPI"></gr-comment-api>
-`);
-
-const basicFixture = fixtureFromElement(commentApiMockElement.is);
-
-suite('gr-patch-range-select tests', () => {
- let element;
-
- let commentApiWrapper;
-
- function getInfo(revisions) {
- const revisionObj = {};
- for (let i = 0; i < revisions.length; i++) {
- revisionObj[i] = revisions[i];
- }
- return new RevisionInfo({revisions: revisionObj});
- }
-
- setup(() => {
- stubRestApi('getDiffComments').returns(Promise.resolve({}));
- stubRestApi('getDiffRobotComments').returns(Promise.resolve({}));
- stubRestApi('getDiffDrafts').returns(Promise.resolve({}));
-
- // Element must be wrapped in an element with direct access to the
- // comment API.
- commentApiWrapper = basicFixture.instantiate();
- element = commentApiWrapper.$.patchRange;
-
- // Stub methods on the changeComments object after changeComments has
- // been initialized.
- element.changeComments = new ChangeComments();
- });
-
- test('enabled/disabled options', () => {
- const patchRange = {
- basePatchNum: 'PARENT',
- patchNum: 3,
- };
- const sortedRevisions = [
- {_number: 3},
- {_number: EditPatchSetNum, basePatchNum: 2},
- {_number: 2},
- {_number: 1},
- ];
- for (const patchNum of ['1', '2', '3']) {
- assert.isFalse(element._computeRightDisabled(patchRange.basePatchNum,
- patchNum, sortedRevisions));
- }
- for (const basePatchNum of ['1', '2']) {
- assert.isFalse(element._computeLeftDisabled(basePatchNum,
- patchRange.patchNum, sortedRevisions));
- }
- assert.isTrue(element._computeLeftDisabled('3', patchRange.patchNum));
-
- patchRange.basePatchNum = EditPatchSetNum;
- assert.isTrue(element._computeLeftDisabled('3', patchRange.patchNum,
- sortedRevisions));
- assert.isTrue(element._computeRightDisabled(patchRange.basePatchNum, '1',
- sortedRevisions));
- assert.isTrue(element._computeRightDisabled(patchRange.basePatchNum, '2',
- sortedRevisions));
- assert.isFalse(element._computeRightDisabled(patchRange.basePatchNum, '3',
- sortedRevisions));
- assert.isTrue(element._computeRightDisabled(patchRange.basePatchNum,
- EditPatchSetNum, sortedRevisions));
- });
-
- test('_computeBaseDropdownContent', () => {
- const availablePatches = [
- {num: 'edit', sha: '1'},
- {num: 3, sha: '2'},
- {num: 2, sha: '3'},
- {num: 1, sha: '4'},
- ];
- const revisions = [
- {
- commit: {parents: []},
- _number: 2,
- description: 'description',
- },
- {commit: {parents: []}},
- {commit: {parents: []}},
- {commit: {parents: []}},
- ];
- element.revisionInfo = getInfo(revisions);
- const patchNum = 1;
- const sortedRevisions = [
- {_number: 3, created: 'Mon, 01 Jan 2001 00:00:00 GMT'},
- {_number: EditPatchSetNum, basePatchNum: 2},
- {_number: 2, description: 'description'},
- {_number: 1},
- ];
- const expectedResult = [
- {
- disabled: true,
- triggerText: 'Patchset edit',
- text: 'Patchset edit | 1',
- mobileText: 'edit',
- bottomText: '',
- value: 'edit',
- },
- {
- disabled: true,
- triggerText: 'Patchset 3',
- text: 'Patchset 3 | 2',
- mobileText: '3',
- bottomText: '',
- value: 3,
- date: 'Mon, 01 Jan 2001 00:00:00 GMT',
- },
- {
- disabled: true,
- triggerText: 'Patchset 2',
- text: 'Patchset 2 | 3',
- mobileText: '2 description',
- bottomText: 'description',
- value: 2,
- },
- {
- disabled: true,
- triggerText: 'Patchset 1',
- text: 'Patchset 1 | 4',
- mobileText: '1',
- bottomText: '',
- value: 1,
- },
- {
- text: 'Base',
- value: 'PARENT',
- },
- ];
- assert.deepEqual(element._computeBaseDropdownContent(availablePatches,
- patchNum, sortedRevisions, element.changeComments,
- element.revisionInfo),
- expectedResult);
- });
-
- test('_computeBaseDropdownContent called when patchNum updates', () => {
- element.revisions = [
- {commit: {parents: []}},
- {commit: {parents: []}},
- {commit: {parents: []}},
- {commit: {parents: []}},
- ];
- element.revisionInfo = getInfo(element.revisions);
- element.availablePatches = [
- {num: 1, sha: '1'},
- {num: 2, sha: '2'},
- {num: 3, sha: '3'},
- {num: 'edit', sha: '4'},
- ];
- element.patchNum = 2;
- element.basePatchNum = 'PARENT';
- flush();
-
- sinon.stub(element, '_computeBaseDropdownContent');
-
- // Should be recomputed for each available patch
- element.set('patchNum', 1);
- assert.equal(element._computeBaseDropdownContent.callCount, 1);
- });
-
- test('_computeBaseDropdownContent called when changeComments update',
- async () => {
- element.revisions = [
- {commit: {parents: []}},
- {commit: {parents: []}},
- {commit: {parents: []}},
- {commit: {parents: []}},
- ];
- element.revisionInfo = getInfo(element.revisions);
- element.availablePatches = [
- {num: 'edit', sha: '1'},
- {num: 3, sha: '2'},
- {num: 2, sha: '3'},
- {num: 1, sha: '4'},
- ];
- element.patchNum = 2;
- element.basePatchNum = 'PARENT';
- await flush();
-
- // Should be recomputed for each available patch
- sinon.stub(element, '_computeBaseDropdownContent');
- assert.equal(element._computeBaseDropdownContent.callCount, 0);
- element.changeComments = new ChangeComments();
- await flush();
- assert.equal(element._computeBaseDropdownContent.callCount, 1);
- });
-
- test('_computePatchDropdownContent called when basePatchNum updates', () => {
- element.revisions = [
- {commit: {parents: []}},
- {commit: {parents: []}},
- {commit: {parents: []}},
- {commit: {parents: []}},
- ];
- element.revisionInfo = getInfo(element.revisions);
- element.availablePatches = [
- {num: 1, sha: '1'},
- {num: 2, sha: '2'},
- {num: 3, sha: '3'},
- {num: 'edit', sha: '4'},
- ];
- element.patchNum = 2;
- element.basePatchNum = 'PARENT';
- flush();
-
- // Should be recomputed for each available patch
- sinon.stub(element, '_computePatchDropdownContent');
- element.set('basePatchNum', 1);
- assert.equal(element._computePatchDropdownContent.callCount, 1);
- });
-
- test('_computePatchDropdownContent', () => {
- const availablePatches = [
- {num: 'edit', sha: '1'},
- {num: 3, sha: '2'},
- {num: 2, sha: '3'},
- {num: 1, sha: '4'},
- ];
- const basePatchNum = 1;
- const sortedRevisions = [
- {_number: 3, created: 'Mon, 01 Jan 2001 00:00:00 GMT'},
- {_number: EditPatchSetNum, basePatchNum: 2},
- {_number: 2, description: 'description'},
- {_number: 1},
- ];
-
- const expectedResult = [
- {
- disabled: false,
- triggerText: 'edit',
- text: 'edit | 1',
- mobileText: 'edit',
- bottomText: '',
- value: 'edit',
- },
- {
- disabled: false,
- triggerText: 'Patchset 3',
- text: 'Patchset 3 | 2',
- mobileText: '3',
- bottomText: '',
- value: 3,
- date: 'Mon, 01 Jan 2001 00:00:00 GMT',
- },
- {
- disabled: false,
- triggerText: 'Patchset 2',
- text: 'Patchset 2 | 3',
- mobileText: '2 description',
- bottomText: 'description',
- value: 2,
- },
- {
- disabled: true,
- triggerText: 'Patchset 1',
- text: 'Patchset 1 | 4',
- mobileText: '1',
- bottomText: '',
- value: 1,
- },
- ];
-
- assert.deepEqual(element._computePatchDropdownContent(availablePatches,
- basePatchNum, sortedRevisions, element.changeComments),
- expectedResult);
- });
-
- test('filesWeblinks', () => {
- element.filesWeblinks = {
- meta_a: [
- {
- name: 'foo',
- url: 'f.oo',
- },
- ],
- meta_b: [
- {
- name: 'bar',
- url: 'ba.r',
- },
- ],
- };
- flush();
- const domApi = dom(element.root);
- assert.equal(
- domApi.querySelector('a[href="f.oo"]').textContent, 'foo');
- assert.equal(
- domApi.querySelector('a[href="ba.r"]').textContent, 'bar');
- });
-
- test('_computePatchSetCommentsString', () => {
- // Test string with unresolved comments.
- const comments = {
- foo: [{
- id: '27dcee4d_f7b77cfa',
- message: 'test',
- patch_set: 1,
- unresolved: true,
- updated: '2017-10-11 20:48:40.000000000',
- }],
- bar: [
- {
- id: '27dcee4d_f7b77cfa',
- message: 'test',
- patch_set: 1,
- updated: '2017-10-12 20:48:40.000000000',
- },
- {
- id: '27dcee4d_f7b77cfa',
- message: 'test',
- patch_set: 1,
- updated: '2017-10-13 20:48:40.000000000',
- },
- ],
- abc: [],
- // Patchset level comment does not contribute to the count
- [SpecialFilePath.PATCHSET_LEVEL_COMMENTS]: [{
- id: '27dcee4d_f7b77cfa',
- message: 'test',
- patch_set: 1,
- unresolved: true,
- updated: '2017-10-11 20:48:40.000000000',
- }],
- };
- element.changeComments = new ChangeComments(comments);
-
- assert.equal(element._computePatchSetCommentsString(
- element.changeComments, 1), ' (3 comments, 1 unresolved)');
-
- // Test string with no unresolved comments.
- delete element.changeComments._comments['foo'];
- assert.equal(element._computePatchSetCommentsString(
- element.changeComments, 1), ' (2 comments)');
-
- // Test string with no comments.
- delete element.changeComments._comments['bar'];
- assert.equal(element._computePatchSetCommentsString(
- element.changeComments, 1), '');
- });
-
- test('patch-range-change fires', () => {
- const handler = sinon.stub();
- element.basePatchNum = 1;
- element.patchNum = 3;
- element.addEventListener('patch-range-change', handler);
-
- element.$.basePatchDropdown._handleValueChange(2, [{value: 2}]);
- assert.isTrue(handler.calledOnce);
- assert.deepEqual(handler.lastCall.args[0].detail,
- {basePatchNum: 2, patchNum: 3});
-
- // BasePatchNum should not have changed, due to one-way data binding.
- element.$.patchNumDropdown._handleValueChange('edit', [{value: 'edit'}]);
- assert.deepEqual(handler.lastCall.args[0].detail,
- {basePatchNum: 1, patchNum: 'edit'});
- });
-});
-
diff --git a/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select_test.ts b/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select_test.ts
new file mode 100644
index 0000000..a47b685
--- /dev/null
+++ b/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select_test.ts
@@ -0,0 +1,491 @@
+/**
+ * @license
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import '../../../test/common-test-setup-karma';
+import '../gr-comment-api/gr-comment-api';
+import '../../shared/revision-info/revision-info';
+import './gr-patch-range-select';
+import {GrPatchRangeSelect} from './gr-patch-range-select';
+import '../../../test/mocks/comment-api';
+import {RevisionInfo as RevisionInfoClass} from '../../shared/revision-info/revision-info';
+import {ChangeComments} from '../gr-comment-api/gr-comment-api';
+import {stubRestApi} from '../../../test/test-utils';
+import {
+ BasePatchSetNum,
+ EditPatchSetNum,
+ PatchSetNum,
+ RevisionInfo,
+ Timestamp,
+ UrlEncodedCommentId,
+ PathToCommentsInfoMap,
+} from '../../../types/common';
+import {EditRevisionInfo, ParsedChangeInfo} from '../../../types/types';
+import {SpecialFilePath} from '../../../constants/constants';
+import {
+ createEditRevision,
+ createRevision,
+} from '../../../test/test-data-generators';
+import {PatchSet} from '../../../utils/patch-set-util';
+import {
+ DropdownItem,
+ GrDropdownList,
+} from '../../shared/gr-dropdown-list/gr-dropdown-list';
+import {queryAndAssert} from '../../../test/test-utils';
+
+const basicFixture = fixtureFromElement('gr-patch-range-select');
+
+type RevIdToRevisionInfo = {
+ [revisionId: string]: RevisionInfo | EditRevisionInfo;
+};
+
+suite('gr-patch-range-select tests', () => {
+ let element: GrPatchRangeSelect;
+
+ function getInfo(revisions: RevisionInfo[]) {
+ const revisionObj: Partial<RevIdToRevisionInfo> = {};
+ for (let i = 0; i < revisions.length; i++) {
+ revisionObj[i] = revisions[i];
+ }
+ return new RevisionInfoClass({revisions: revisionObj} as ParsedChangeInfo);
+ }
+
+ setup(async () => {
+ stubRestApi('getDiffComments').returns(Promise.resolve({}));
+ stubRestApi('getDiffRobotComments').returns(Promise.resolve({}));
+ stubRestApi('getDiffDrafts').returns(Promise.resolve({}));
+
+ // Element must be wrapped in an element with direct access to the
+ // comment API.
+ element = basicFixture.instantiate();
+
+ // Stub methods on the changeComments object after changeComments has
+ // been initialized.
+ element.changeComments = new ChangeComments();
+ await element.updateComplete;
+ });
+
+ test('enabled/disabled options', () => {
+ const patchRange = {
+ basePatchNum: 'PARENT' as PatchSetNum,
+ patchNum: 3 as PatchSetNum,
+ };
+ const sortedRevisions = [
+ createRevision(3) as RevisionInfo,
+ createEditRevision(2) as EditRevisionInfo,
+ createRevision(2) as RevisionInfo,
+ createRevision(1) as RevisionInfo,
+ ];
+ for (const patchNum of [1, 2, 3]) {
+ assert.isFalse(
+ element._computeRightDisabled(
+ patchRange.basePatchNum,
+ patchNum as PatchSetNum,
+ sortedRevisions
+ )
+ );
+ }
+ for (const basePatchNum of [1, 2]) {
+ assert.isFalse(
+ element._computeLeftDisabled(
+ basePatchNum as PatchSetNum,
+ patchRange.patchNum,
+ sortedRevisions
+ )
+ );
+ }
+ assert.isTrue(
+ element._computeLeftDisabled(3 as PatchSetNum, patchRange.patchNum, [])
+ );
+
+ patchRange.basePatchNum = EditPatchSetNum;
+ assert.isTrue(
+ element._computeLeftDisabled(
+ 3 as PatchSetNum,
+ patchRange.patchNum,
+ sortedRevisions
+ )
+ );
+ assert.isTrue(
+ element._computeRightDisabled(
+ patchRange.basePatchNum,
+ 1 as PatchSetNum,
+ sortedRevisions
+ )
+ );
+ assert.isTrue(
+ element._computeRightDisabled(
+ patchRange.basePatchNum,
+ 2 as PatchSetNum,
+ sortedRevisions
+ )
+ );
+ assert.isFalse(
+ element._computeRightDisabled(
+ patchRange.basePatchNum,
+ 3 as PatchSetNum,
+ sortedRevisions
+ )
+ );
+ assert.isTrue(
+ element._computeRightDisabled(
+ patchRange.basePatchNum,
+ EditPatchSetNum,
+ sortedRevisions
+ )
+ );
+ });
+
+ test('_computeBaseDropdownContent', () => {
+ const availablePatches = [
+ {num: 'edit', sha: '1'} as PatchSet,
+ {num: 3, sha: '2'} as PatchSet,
+ {num: 2, sha: '3'} as PatchSet,
+ {num: 1, sha: '4'} as PatchSet,
+ ];
+ const revisions: RevisionInfo[] = [
+ createRevision(2),
+ createRevision(3),
+ createRevision(1),
+ createRevision(4),
+ ];
+ element.revisionInfo = getInfo(revisions);
+ const sortedRevisions = [
+ createRevision(3) as RevisionInfo,
+ createEditRevision(2) as EditRevisionInfo,
+ createRevision(2) as RevisionInfo,
+ createRevision(1) as RevisionInfo,
+ ];
+ const expectedResult: DropdownItem[] = [
+ {
+ disabled: true,
+ triggerText: 'Patchset edit',
+ text: 'Patchset edit | 1',
+ mobileText: 'edit',
+ bottomText: '',
+ value: 'edit',
+ },
+ {
+ disabled: true,
+ triggerText: 'Patchset 3',
+ text: 'Patchset 3 | 2',
+ mobileText: '3',
+ bottomText: '',
+ value: 3,
+ date: '2020-02-01 01:02:03.000000000' as Timestamp,
+ } as DropdownItem,
+ {
+ disabled: true,
+ triggerText: 'Patchset 2',
+ text: 'Patchset 2 | 3',
+ mobileText: '2',
+ bottomText: '',
+ value: 2,
+ date: '2020-02-01 01:02:03.000000000' as Timestamp,
+ } as DropdownItem,
+ {
+ disabled: true,
+ triggerText: 'Patchset 1',
+ text: 'Patchset 1 | 4',
+ mobileText: '1',
+ bottomText: '',
+ value: 1,
+ date: '2020-02-01 01:02:03.000000000' as Timestamp,
+ } as DropdownItem,
+ {
+ text: 'Base',
+ value: 'PARENT',
+ } as DropdownItem,
+ ];
+ assert.deepEqual(
+ element._computeBaseDropdownContent(
+ availablePatches,
+ 1 as PatchSetNum,
+ sortedRevisions,
+ element.changeComments,
+ element.revisionInfo
+ ),
+ expectedResult
+ );
+ });
+
+ test('_computeBaseDropdownContent called when patchNum updates', async () => {
+ element.revisions = [
+ createRevision(2),
+ createRevision(3),
+ createRevision(1),
+ createRevision(4),
+ ];
+ element.revisionInfo = getInfo(element.revisions);
+ element.availablePatches = [
+ {num: 1, sha: '1'} as PatchSet,
+ {num: 2, sha: '2'} as PatchSet,
+ {num: 3, sha: '3'} as PatchSet,
+ {num: 'edit', sha: '4'} as PatchSet,
+ ];
+ element.patchNum = 2 as PatchSetNum;
+ element.basePatchNum = 'PARENT' as BasePatchSetNum;
+ await element.updateComplete;
+
+ const baseDropDownStub = sinon.stub(element, '_computeBaseDropdownContent');
+
+ // Should be recomputed for each available patch
+ element.patchNum = 1 as PatchSetNum;
+ await element.updateComplete;
+ assert.equal(baseDropDownStub.callCount, 1);
+ });
+
+ test('_computeBaseDropdownContent called when changeComments update', async () => {
+ element.revisions = [
+ createRevision(2),
+ createRevision(3),
+ createRevision(1),
+ createRevision(4),
+ ];
+ element.revisionInfo = getInfo(element.revisions);
+ element.availablePatches = [
+ {num: 3, sha: '2'} as PatchSet,
+ {num: 2, sha: '3'} as PatchSet,
+ {num: 1, sha: '4'} as PatchSet,
+ ];
+ element.patchNum = 2 as PatchSetNum;
+ element.basePatchNum = 'PARENT' as BasePatchSetNum;
+ await element.updateComplete;
+
+ // Should be recomputed for each available patch
+ const baseDropDownStub = sinon.stub(element, '_computeBaseDropdownContent');
+ assert.equal(baseDropDownStub.callCount, 0);
+ element.changeComments = new ChangeComments();
+ await element.updateComplete;
+ assert.equal(baseDropDownStub.callCount, 1);
+ });
+
+ test('_computePatchDropdownContent called when basePatchNum updates', async () => {
+ element.revisions = [
+ createRevision(2),
+ createRevision(3),
+ createRevision(1),
+ createRevision(4),
+ ];
+ element.revisionInfo = getInfo(element.revisions);
+ element.availablePatches = [
+ {num: 1, sha: '1'} as PatchSet,
+ {num: 2, sha: '2'} as PatchSet,
+ {num: 3, sha: '3'} as PatchSet,
+ {num: 'edit', sha: '4'} as PatchSet,
+ ];
+ element.patchNum = 2 as PatchSetNum;
+ element.basePatchNum = 'PARENT' as BasePatchSetNum;
+ await element.updateComplete;
+
+ // Should be recomputed for each available patch
+ const baseDropDownStub = sinon.stub(
+ element,
+ '_computePatchDropdownContent'
+ );
+ element.basePatchNum = 1 as BasePatchSetNum;
+ await element.updateComplete;
+ assert.equal(baseDropDownStub.callCount, 1);
+ });
+
+ test('_computePatchDropdownContent', () => {
+ const availablePatches: PatchSet[] = [
+ {num: 'edit', sha: '1'} as PatchSet,
+ {num: 3, sha: '2'} as PatchSet,
+ {num: 2, sha: '3'} as PatchSet,
+ {num: 1, sha: '4'} as PatchSet,
+ ];
+ const basePatchNum = 1;
+ const sortedRevisions = [
+ createRevision(3) as RevisionInfo,
+ createEditRevision(2) as EditRevisionInfo,
+ createRevision(2, 'description') as RevisionInfo,
+ createRevision(1) as RevisionInfo,
+ ];
+
+ const expectedResult: DropdownItem[] = [
+ {
+ disabled: false,
+ triggerText: 'edit',
+ text: 'edit | 1',
+ mobileText: 'edit',
+ bottomText: '',
+ value: 'edit',
+ },
+ {
+ disabled: false,
+ triggerText: 'Patchset 3',
+ text: 'Patchset 3 | 2',
+ mobileText: '3',
+ bottomText: '',
+ value: 3,
+ date: '2020-02-01 01:02:03.000000000' as Timestamp,
+ } as DropdownItem,
+ {
+ disabled: false,
+ triggerText: 'Patchset 2',
+ text: 'Patchset 2 | 3',
+ mobileText: '2 description',
+ bottomText: 'description',
+ value: 2,
+ date: '2020-02-01 01:02:03.000000000' as Timestamp,
+ } as DropdownItem,
+ {
+ disabled: true,
+ triggerText: 'Patchset 1',
+ text: 'Patchset 1 | 4',
+ mobileText: '1',
+ bottomText: '',
+ value: 1,
+ date: '2020-02-01 01:02:03.000000000' as Timestamp,
+ } as DropdownItem,
+ ];
+
+ assert.deepEqual(
+ element._computePatchDropdownContent(
+ availablePatches,
+ basePatchNum as BasePatchSetNum,
+ sortedRevisions,
+ element.changeComments
+ ),
+ expectedResult
+ );
+ });
+
+ test('filesWeblinks', async () => {
+ element.filesWeblinks = {
+ meta_a: [
+ {
+ name: 'foo',
+ url: 'f.oo',
+ },
+ ],
+ meta_b: [
+ {
+ name: 'bar',
+ url: 'ba.r',
+ },
+ ],
+ };
+ await element.updateComplete;
+ assert.equal(
+ queryAndAssert(element, 'a[href="f.oo"]').textContent!.trim(),
+ 'foo'
+ );
+ assert.equal(
+ queryAndAssert(element, 'a[href="ba.r"]').textContent!.trim(),
+ 'bar'
+ );
+ });
+
+ test('_computePatchSetCommentsString', () => {
+ // Test string with unresolved comments.
+ const comments: PathToCommentsInfoMap = {
+ foo: [
+ {
+ id: '27dcee4d_f7b77cfa' as UrlEncodedCommentId,
+ message: 'test',
+ patch_set: 1 as PatchSetNum,
+ unresolved: true,
+ updated: '2017-10-11 20:48:40.000000000' as Timestamp,
+ },
+ ],
+ bar: [
+ {
+ id: '27dcee4d_f7b77cfa' as UrlEncodedCommentId,
+ message: 'test',
+ patch_set: 1 as PatchSetNum,
+ updated: '2017-10-12 20:48:40.000000000' as Timestamp,
+ },
+ {
+ id: '27dcee4d_f7b77cfa' as UrlEncodedCommentId,
+ message: 'test',
+ patch_set: 1 as PatchSetNum,
+ updated: '2017-10-13 20:48:40.000000000' as Timestamp,
+ },
+ ],
+ abc: [],
+ // Patchset level comment does not contribute to the count
+ [SpecialFilePath.PATCHSET_LEVEL_COMMENTS]: [
+ {
+ id: '27dcee4d_f7b77cfa' as UrlEncodedCommentId,
+ message: 'test',
+ patch_set: 1 as PatchSetNum,
+ unresolved: true,
+ updated: '2017-10-11 20:48:40.000000000' as Timestamp,
+ },
+ ],
+ };
+ element.changeComments = new ChangeComments(comments);
+
+ assert.equal(
+ element._computePatchSetCommentsString(
+ element.changeComments,
+ 1 as PatchSetNum
+ ),
+ ' (3 comments, 1 unresolved)'
+ );
+
+ // Test string with no unresolved comments.
+ delete comments['foo'];
+ element.changeComments = new ChangeComments(comments);
+ assert.equal(
+ element._computePatchSetCommentsString(
+ element.changeComments,
+ 1 as PatchSetNum
+ ),
+ ' (2 comments)'
+ );
+
+ // Test string with no comments.
+ delete comments['bar'];
+ element.changeComments = new ChangeComments(comments);
+ assert.equal(
+ element._computePatchSetCommentsString(
+ element.changeComments,
+ 1 as PatchSetNum
+ ),
+ ''
+ );
+ });
+
+ test('patch-range-change fires', () => {
+ const handler = sinon.stub();
+ element.basePatchNum = 1 as BasePatchSetNum;
+ element.patchNum = 3 as PatchSetNum;
+ element.addEventListener('patch-range-change', handler);
+
+ queryAndAssert<GrDropdownList>(
+ element,
+ '#basePatchDropdown'
+ )._handleValueChange('2', [{text: '', value: '2'}]);
+ assert.isTrue(handler.calledOnce);
+ assert.deepEqual(handler.lastCall.args[0].detail, {
+ basePatchNum: 2,
+ patchNum: 3,
+ });
+
+ // BasePatchNum should not have changed, due to one-way data binding.
+ queryAndAssert<GrDropdownList>(
+ element,
+ '#patchNumDropdown'
+ )._handleValueChange('edit', [{text: '', value: 'edit'}]);
+ assert.deepEqual(handler.lastCall.args[0].detail, {
+ basePatchNum: 1,
+ patchNum: 'edit',
+ });
+ });
+});
diff --git a/polygerrit-ui/app/elements/diff/gr-range-header/gr-range-header.ts b/polygerrit-ui/app/elements/diff/gr-range-header/gr-range-header.ts
index dcf7236..8ce8ce2 100644
--- a/polygerrit-ui/app/elements/diff/gr-range-header/gr-range-header.ts
+++ b/polygerrit-ui/app/elements/diff/gr-range-header/gr-range-header.ts
@@ -55,7 +55,7 @@
override render() {
const icon = this.icon ?? '';
return html` <div class="row">
- <iron-icon class="icon" .icon=${icon}></iron-icon>
+ <iron-icon class="icon" .icon=${icon} aria-hidden="true"></iron-icon>
<slot></slot>
</div>`;
}
diff --git a/polygerrit-ui/app/elements/diff/gr-syntax-layer/gr-syntax-layer.ts b/polygerrit-ui/app/elements/diff/gr-syntax-layer/gr-syntax-layer.ts
index ad671da..dddd6a4 100644
--- a/polygerrit-ui/app/elements/diff/gr-syntax-layer/gr-syntax-layer.ts
+++ b/polygerrit-ui/app/elements/diff/gr-syntax-layer/gr-syntax-layer.ts
@@ -54,6 +54,7 @@
['text/x-erlang', 'erlang'],
['text/x-fortran', 'fortran'],
['text/x-fsharp', 'fsharp'],
+ ['text/x-gherkin', 'gherkin'],
['text/x-go', 'go'],
['text/x-groovy', 'groovy'],
['text/x-haml', 'haml'],
diff --git a/polygerrit-ui/app/elements/edit/gr-editor-view/gr-editor-view.ts b/polygerrit-ui/app/elements/edit/gr-editor-view/gr-editor-view.ts
index 24ebd67..a295588 100644
--- a/polygerrit-ui/app/elements/edit/gr-editor-view/gr-editor-view.ts
+++ b/polygerrit-ui/app/elements/edit/gr-editor-view/gr-editor-view.ts
@@ -259,7 +259,14 @@
_viewEditInChangeView() {
if (this._change)
- GerritNav.navigateToChange(this._change, undefined, undefined, true);
+ GerritNav.navigateToChange(
+ this._change,
+ undefined,
+ undefined,
+ true,
+ undefined,
+ true
+ );
}
_getFileData(
diff --git a/polygerrit-ui/app/elements/gr-app-types.ts b/polygerrit-ui/app/elements/gr-app-types.ts
index 6c8bdb9..39070bd 100644
--- a/polygerrit-ui/app/elements/gr-app-types.ts
+++ b/polygerrit-ui/app/elements/gr-app-types.ts
@@ -124,8 +124,9 @@
edit?: boolean;
patchNum?: RevisionPatchSetNum;
basePatchNum?: BasePatchSetNum;
- queryMap?: Map<string, string> | URLSearchParams;
commentId?: UrlEncodedCommentId;
+ forceReload?: boolean;
+ tab?: string;
}
export interface AppElementJustRegisteredParams {
diff --git a/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view.ts b/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view.ts
index 256e956..25e9de8 100644
--- a/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view.ts
+++ b/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view.ts
@@ -20,11 +20,8 @@
import '../../../styles/gr-form-styles';
import '../../../styles/gr-menu-page-styles';
import '../../../styles/gr-page-nav-styles';
+import '../../../styles/gr-paper-styles';
import '../../../styles/shared-styles';
-import {
- applyTheme as applyDarkTheme,
- removeTheme as removeDarkTheme,
-} from '../../../styles/themes/dark-theme';
import '../../plugins/gr-endpoint-decorator/gr-endpoint-decorator';
import '../gr-change-table-editor/gr-change-table-editor';
import '../../shared/gr-button/gr-button';
@@ -74,6 +71,7 @@
TimeFormat,
} from '../../../constants/constants';
import {columnNames} from '../../change-list/gr-change-list/gr-change-list';
+import {windowLocationReload} from '../../../utils/dom-util';
const PREFS_SECTION_FIELDS: Array<keyof PreferencesInput> = [
'changes_per_page',
@@ -243,7 +241,6 @@
this.$.groupList.loadData(),
this.$.identities.loadData(),
this.$.editPrefs.loadData(),
- this.$.diffPrefs.loadData(),
];
// TODO(dhruvsri): move this to the service
@@ -537,13 +534,14 @@
_handleToggleDark() {
if (this._isDark) {
window.localStorage.removeItem('dark-theme');
- removeDarkTheme();
} else {
window.localStorage.setItem('dark-theme', 'true');
- applyDarkTheme();
}
- this._isDark = !!window.localStorage.getItem('dark-theme');
- fireAlert(this, `Theme changed to ${this._isDark ? 'dark' : 'light'}.`);
+ this.reloadPage();
+ }
+
+ reloadPage() {
+ windowLocationReload();
}
_showHttpAuth(config?: ServerInfo) {
diff --git a/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view_html.ts b/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view_html.ts
index 78c4a62..c1ebcac 100644
--- a/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view_html.ts
+++ b/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view_html.ts
@@ -20,6 +20,9 @@
<style include="gr-font-styles">
/* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
</style>
+ <style include="gr-paper-styles">
+ /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
+ </style>
<style include="shared-styles">
:host {
color: var(--primary-text-color);
@@ -100,6 +103,7 @@
</gr-page-nav>
<div class="main gr-form-styles">
<h1 class="heading-1">User Settings</h1>
+ <h2 id="Theme">Theme</h2>
<section class="darkToggle">
<div class="toggle">
<paper-toggle-button
@@ -108,13 +112,10 @@
on-change="_handleToggleDark"
on-click="_onTapDarkToggle"
></paper-toggle-button>
- <div id="darkThemeToggleLabel">Dark theme (alpha)</div>
+ <div id="darkThemeToggleLabel">
+ Dark theme (the toggle reloads the page)
+ </div>
</div>
- <p>
- Gerrit's dark theme is in early alpha, and almost definitely will not
- play nicely with themes set by specific Gerrit hosts. Filing feedback
- via the link in the app footer is strongly encouraged!
- </p>
</section>
<h2 id="Profile" class$="[[_computeHeaderClass(_accountInfoChanged)]]">
Profile
diff --git a/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view_test.ts b/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view_test.ts
index 1165f1e..61876fe 100644
--- a/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view_test.ts
+++ b/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view_test.ts
@@ -16,7 +16,6 @@
*/
import '../../../test/common-test-setup-karma';
-import {getComputedStyleValue} from '../../../utils/dom-util';
import './gr-settings-view';
import {GrSettingsView} from './gr-settings-view';
import {flush} from '@polymer/polymer/lib/legacy/polymer.dom';
@@ -130,23 +129,24 @@
await element._testOnly_loadingPromise;
});
- test('theme changing', () => {
+ test('theme changing', async () => {
+ const reloadStub = sinon.stub(element, 'reloadPage');
+
window.localStorage.removeItem('dark-theme');
assert.isFalse(window.localStorage.getItem('dark-theme') === 'true');
const themeToggle = queryAndAssert(
element,
'.darkToggle paper-toggle-button'
);
- /* const themeToggle = element.shadowRoot
- .querySelector('.darkToggle paper-toggle-button'); */
MockInteractions.tap(themeToggle);
assert.isTrue(window.localStorage.getItem('dark-theme') === 'true');
- assert.equal(
- getComputedStyleValue('--primary-text-color', document.body),
- '#e8eaed'
- );
+ assert.isTrue(reloadStub.calledOnce);
+
+ element._isDark = true;
+ await flush();
MockInteractions.tap(themeToggle);
assert.isFalse(window.localStorage.getItem('dark-theme') === 'true');
+ assert.isTrue(reloadStub.calledTwice);
});
test('calls the title-change event', () => {
diff --git a/polygerrit-ui/app/elements/shared/gr-account-label/gr-account-label.ts b/polygerrit-ui/app/elements/shared/gr-account-label/gr-account-label.ts
index dabf761..0283ca4 100644
--- a/polygerrit-ui/app/elements/shared/gr-account-label/gr-account-label.ts
+++ b/polygerrit-ui/app/elements/shared/gr-account-label/gr-account-label.ts
@@ -261,7 +261,7 @@
${!this.hideStatus && account.status
? html`<iron-icon
class="status"
- icon="gr-icons:calendar"
+ icon="gr-icons:unavailable"
></iron-icon>`
: ''}
</span>
diff --git a/polygerrit-ui/app/elements/shared/gr-change-star/gr-change-star.ts b/polygerrit-ui/app/elements/shared/gr-change-star/gr-change-star.ts
index a23621e..c6fd01c 100644
--- a/polygerrit-ui/app/elements/shared/gr-change-star/gr-change-star.ts
+++ b/polygerrit-ui/app/elements/shared/gr-change-star/gr-change-star.ts
@@ -15,10 +15,6 @@
* limitations under the License.
*/
import '../gr-icons/gr-icons';
-import '../../../styles/shared-styles';
-import {PolymerElement} from '@polymer/polymer/polymer-element';
-import {htmlTemplate} from './gr-change-star_html';
-import {customElement, property} from '@polymer/decorators';
import {ChangeInfo} from '../../../types/common';
import {fireAlert} from '../../../utils/event-util';
import {
@@ -26,6 +22,9 @@
ShortcutSection,
} from '../../../services/shortcuts/shortcuts-config';
import {appContext} from '../../../services/app-context';
+import {sharedStyles} from '../../../styles/shared-styles';
+import {LitElement, css, html} from 'lit';
+import {customElement, property} from 'lit/decorators';
declare global {
interface HTMLElementTagNameMap {
@@ -39,44 +38,78 @@
}
@customElement('gr-change-star')
-export class GrChangeStar extends PolymerElement {
- static get template() {
- return htmlTemplate;
- }
-
+export class GrChangeStar extends LitElement {
/**
* Fired when star state is toggled.
*
* @event toggle-star
*/
- @property({type: Object, notify: true})
+ @property({type: Object})
change?: ChangeInfo;
private readonly shortcuts = appContext.shortcutsService;
- _computeStarClass(starred?: boolean) {
- return starred ? 'active' : '';
+ static override get styles() {
+ return [
+ sharedStyles,
+ css`
+ button {
+ background-color: transparent;
+ cursor: pointer;
+ }
+ iron-icon.active {
+ fill: var(--link-color);
+ }
+ iron-icon {
+ vertical-align: top;
+ --iron-icon-height: var(
+ --gr-change-star-size,
+ var(--line-height-normal, 20px)
+ );
+ --iron-icon-width: var(
+ --gr-change-star-size,
+ var(--line-height-normal, 20px)
+ );
+ }
+ :host([hidden]) {
+ visibility: hidden;
+ display: block !important;
+ }
+ `,
+ ];
}
- _computeStarIcon(starred?: boolean) {
- // Hollow star is used to indicate inactive state.
- return `gr-icons:star${starred ? '' : '-border'}`;
- }
-
- _computeAriaLabel(starred?: boolean) {
- return starred ? 'Unstar this change' : 'Star this change';
+ override render() {
+ return html`
+ <button
+ role="checkbox"
+ title=${this.shortcuts.createTitle(
+ Shortcut.TOGGLE_CHANGE_STAR,
+ ShortcutSection.ACTIONS
+ )}
+ aria-label=${this.change?.starred
+ ? 'Unstar this change'
+ : 'Star this change'}
+ @click=${this.toggleStar}
+ >
+ <iron-icon
+ class=${this.change?.starred ? 'active' : ''}
+ .icon=${`gr-icons:star${this.change?.starred ? '' : '-border'}`}
+ ></iron-icon>
+ </button>
+ `;
}
toggleStar() {
// Note: change should always be defined when use gr-change-star
// but since we don't have a good way to enforce usage to always
// set the change, we still check it here.
- if (!this.change) {
- return;
- }
+ if (!this.change) return;
+
const newVal = !this.change.starred;
- this.set('change.starred', newVal);
+ this.change.starred = newVal;
+ this.requestUpdate('change');
const detail: ChangeStarToggleStarDetail = {
change: this.change,
starred: newVal,
@@ -90,8 +123,4 @@
})
);
}
-
- createTitle(shortcutName: Shortcut, section: ShortcutSection) {
- return this.shortcuts.createTitle(shortcutName, section);
- }
}
diff --git a/polygerrit-ui/app/elements/shared/gr-change-star/gr-change-star_html.ts b/polygerrit-ui/app/elements/shared/gr-change-star/gr-change-star_html.ts
deleted file mode 100644
index d404795..0000000
--- a/polygerrit-ui/app/elements/shared/gr-change-star/gr-change-star_html.ts
+++ /dev/null
@@ -1,56 +0,0 @@
-/**
- * @license
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-import {html} from '@polymer/polymer/lib/utils/html-tag';
-
-export const htmlTemplate = html`
- <style include="shared-styles">
- button {
- background-color: transparent;
- cursor: pointer;
- }
- iron-icon.active {
- fill: var(--link-color);
- }
- iron-icon {
- vertical-align: top;
- --iron-icon-height: var(
- --gr-change-star-size,
- var(--line-height-normal, 20px)
- );
- --iron-icon-width: var(
- --gr-change-star-size,
- var(--line-height-normal, 20px)
- );
- }
- :host([hidden]) {
- visibility: hidden;
- display: block !important;
- }
- </style>
- <button
- role="checkbox"
- title="[[createTitle(Shortcut.TOGGLE_CHANGE_STAR,
- ShortcutSection.ACTIONS)]]"
- aria-label="[[_computeAriaLabel(change.starred)]]"
- on-click="toggleStar"
- >
- <iron-icon
- class$="[[_computeStarClass(change.starred)]]"
- icon$="[[_computeStarIcon(change.starred)]]"
- ></iron-icon>
- </button>
-`;
diff --git a/polygerrit-ui/app/elements/shared/gr-change-star/gr-change-star_test.ts b/polygerrit-ui/app/elements/shared/gr-change-star/gr-change-star_test.ts
index 8f411ae..2c5d7a2 100644
--- a/polygerrit-ui/app/elements/shared/gr-change-star/gr-change-star_test.ts
+++ b/polygerrit-ui/app/elements/shared/gr-change-star/gr-change-star_test.ts
@@ -27,45 +27,47 @@
suite('gr-change-star tests', () => {
let element: GrChangeStar;
- setup(() => {
+ setup(async () => {
element = basicFixture.instantiate();
element.change = {
...createChange(),
starred: true,
};
+ await element.updateComplete;
});
test('star visibility states', async () => {
- element.set('change.starred', true);
- await flush();
+ element.change!.starred = true;
+ await element.updateComplete;
let icon = queryAndAssert<IronIconElement>(element, 'iron-icon');
assert.isTrue(icon.classList.contains('active'));
assert.equal(icon.icon, 'gr-icons:star');
- element.set('change.starred', false);
- await flush();
+ element.change!.starred = false;
+ element.requestUpdate('change');
+ await element.updateComplete;
icon = queryAndAssert<IronIconElement>(element, 'iron-icon');
assert.isFalse(icon.classList.contains('active'));
assert.equal(icon.icon, 'gr-icons:star-border');
});
test('starring', async () => {
- element.set('change.starred', false);
- await flush();
+ element.change!.starred = false;
+ await element.updateComplete;
assert.equal(element.change!.starred, false);
- MockInteractions.tap(queryAndAssert(element, 'button'));
- await flush();
+ MockInteractions.tap(queryAndAssert<HTMLButtonElement>(element, 'button'));
+ await element.updateComplete;
assert.equal(element.change!.starred, true);
});
test('unstarring', async () => {
- element.set('change.starred', true);
- await flush();
+ element.change!.starred = true;
+ await element.updateComplete;
assert.equal(element.change!.starred, true);
- MockInteractions.tap(queryAndAssert(element, 'button'));
- await flush();
+ MockInteractions.tap(queryAndAssert<HTMLButtonElement>(element, 'button'));
+ await element.updateComplete;
assert.equal(element.change!.starred, false);
});
});
diff --git a/polygerrit-ui/app/elements/shared/gr-comment-thread/gr-comment-thread.ts b/polygerrit-ui/app/elements/shared/gr-comment-thread/gr-comment-thread.ts
index 6b2e5c4..7ecced0 100644
--- a/polygerrit-ui/app/elements/shared/gr-comment-thread/gr-comment-thread.ts
+++ b/polygerrit-ui/app/elements/shared/gr-comment-thread/gr-comment-thread.ts
@@ -41,7 +41,7 @@
SpecialFilePath,
} from '../../../constants/constants';
import {computeDisplayPath} from '../../../utils/path-list-util';
-import {computed, customElement, observe, property} from '@polymer/decorators';
+import {customElement, observe, property} from '@polymer/decorators';
import {
AccountDetailInfo,
CommentRange,
@@ -55,7 +55,6 @@
import {PolymerDeepPropertyChange} from '@polymer/polymer/interfaces';
import {FILE, LineNumber} from '../../diff/gr-diff/gr-diff-line';
import {GrButton} from '../gr-button/gr-button';
-import {KnownExperimentId} from '../../../services/flags/flags';
import {DiffInfo, DiffPreferencesInfo} from '../../../types/diff';
import {DiffLayer, RenderPreferences} from '../../../api/diff';
import {
@@ -201,13 +200,14 @@
@property({type: Array})
layers: DiffLayer[] = [];
+ @property({type: Object, computed: 'computeDiff(comments, path)'})
+ _diff?: DiffInfo;
+
/** Called in disconnectedCallback. */
private cleanups: (() => void)[] = [];
private readonly reporting = appContext.reportingService;
- private readonly flagsService = appContext.flagsService;
-
private readonly commentsService = appContext.commentsService;
readonly storage = appContext.storageService;
@@ -261,15 +261,19 @@
this._setInitialExpandedState();
}
- @computed('comments', 'path')
- get _diff() {
- if (this.comments === undefined || this.path === undefined) return;
- if (!this.comments[0]?.context_lines?.length) return;
+ computeDiff(comments?: UIComment[], path?: string) {
+ if (comments === undefined || path === undefined) return undefined;
+ if (!comments[0]?.context_lines?.length) return undefined;
const diff = computeDiffFromContext(
- this.comments[0].context_lines,
- this.path,
- this.comments[0].source_content_type
+ comments[0].context_lines,
+ path,
+ comments[0].source_content_type
);
+ // Do we really have to re-compute (and re-render) the diff?
+ if (this._diff && JSON.stringify(this._diff) === JSON.stringify(diff)) {
+ return this._diff;
+ }
+
if (!anyLineTooLong(diff)) {
this.syntaxLayer.init(diff);
waitForEventOnce(this, 'render').then(() => {
@@ -370,10 +374,7 @@
}
_initLayers(disableTokenHighlighting: boolean) {
- if (
- this.flagsService.isEnabled(KnownExperimentId.TOKEN_HIGHLIGHTING) &&
- !disableTokenHighlighting
- ) {
+ if (!disableTokenHighlighting) {
this.layers.push(new TokenHighlightLayer(this));
}
this.layers.push(this.syntaxLayer);
diff --git a/polygerrit-ui/app/elements/shared/gr-comment/gr-comment.ts b/polygerrit-ui/app/elements/shared/gr-comment/gr-comment.ts
index 154a045..4f6702d 100644
--- a/polygerrit-ui/app/elements/shared/gr-comment/gr-comment.ts
+++ b/polygerrit-ui/app/elements/shared/gr-comment/gr-comment.ts
@@ -38,11 +38,11 @@
import {GrOverlay} from '../gr-overlay/gr-overlay';
import {
AccountDetailInfo,
- NumericChangeId,
+ BasePatchSetNum,
ConfigInfo,
+ NumericChangeId,
PatchSetNum,
RepoName,
- BasePatchSetNum,
} from '../../../types/common';
import {GrButton} from '../gr-button/gr-button';
import {GrConfirmDeleteCommentDialog} from '../gr-confirm-delete-comment-dialog/gr-confirm-delete-comment-dialog';
@@ -60,6 +60,7 @@
import {debounce, DelayedTask} from '../../../utils/async-util';
import {StorageLocation} from '../../../services/storage/gr-storage';
import {addShortcut, Key, Modifier} from '../../../utils/dom-util';
+import {Interaction} from '../../../constants/reporting';
const STORAGE_DEBOUNCE_INTERVAL = 400;
const TOAST_DEBOUNCE_INTERVAL = 200;
@@ -489,6 +490,8 @@
return this._discardDraft();
}
+ const details = this.commentDetailsForReporting();
+ this.reporting.reportInteraction(Interaction.SAVE_COMMENT, details);
this._xhrPromise = this._saveDraft(comment)
.then(response => {
this.disabled = false;
@@ -508,6 +511,8 @@
}
if (!resComment.patch_set) resComment.patch_set = this.patchNum;
this.comment = resComment;
+ const details = this.commentDetailsForReporting();
+ this.reporting.reportInteraction(Interaction.COMMENT_SAVED, details);
this._fireSave();
return obj;
});
@@ -520,6 +525,17 @@
return this._xhrPromise;
}
+ private commentDetailsForReporting() {
+ return {
+ id: this.comment?.id,
+ message_length: this.comment?.message?.length,
+ in_reply_to: this.comment?.in_reply_to,
+ unresolved: this.comment?.unresolved,
+ path_length: this.comment?.path?.length,
+ line: this.comment?.range?.start_line ?? this.comment?.line,
+ };
+ }
+
_eraseDraftCommentFromStorage() {
// Prevents a race condition in which removing the draft comment occurs
// prior to it being saved.
@@ -765,7 +781,7 @@
const timer = this.reporting.getTimer(timingLabel);
this.set('comment.__editing', false);
return this.save().then(() => {
- timer.end();
+ timer.end({id: this.comment?.id});
});
}
@@ -849,7 +865,7 @@
if (!response.ok) {
this.discarding = false;
}
- timer.end();
+ timer.end({id: this.comment?.id});
this._fireDiscard();
return response;
})
diff --git a/polygerrit-ui/app/elements/shared/gr-diff-preferences/gr-diff-preferences.ts b/polygerrit-ui/app/elements/shared/gr-diff-preferences/gr-diff-preferences.ts
index e560773..6ddaccf 100644
--- a/polygerrit-ui/app/elements/shared/gr-diff-preferences/gr-diff-preferences.ts
+++ b/polygerrit-ui/app/elements/shared/gr-diff-preferences/gr-diff-preferences.ts
@@ -24,6 +24,9 @@
import {DiffPreferencesInfo, IgnoreWhitespaceType} from '../../../types/diff';
import {GrSelect} from '../gr-select/gr-select';
import {appContext} from '../../../services/app-context';
+import {diffPreferences$} from '../../../services/user/user-model';
+import {takeUntil} from 'rxjs/operators';
+import {Subject} from 'rxjs';
export interface GrDiffPreferences {
$: {
@@ -39,7 +42,7 @@
contextSelect: GrSelect;
ignoreWhiteSpace: HTMLInputElement;
};
- save(): Promise<void>;
+ save(): void;
}
@customElement('gr-diff-preferences')
@@ -54,12 +57,22 @@
@property({type: Object})
diffPrefs?: DiffPreferencesInfo;
- private readonly restApiService = appContext.restApiService;
+ private readonly userService = appContext.userService;
- loadData() {
- return this.restApiService.getDiffPreferences().then(prefs => {
- this.diffPrefs = prefs;
- });
+ private readonly disconnected$ = new Subject();
+
+ override connectedCallback() {
+ super.connectedCallback();
+ diffPreferences$
+ .pipe(takeUntil(this.disconnected$))
+ .subscribe(diffPreferences => {
+ this.diffPrefs = diffPreferences;
+ });
+ }
+
+ override disconnectedCallback() {
+ this.disconnected$.next();
+ super.disconnectedCallback();
}
_handleDiffPrefsChanged() {
@@ -125,12 +138,10 @@
this._handleDiffPrefsChanged();
}
- save() {
- if (!this.diffPrefs)
- return Promise.reject(new Error('Missing diff preferences'));
- return this.restApiService.saveDiffPreferences(this.diffPrefs).then(_ => {
- this.hasUnsavedChanges = false;
- });
+ async save() {
+ if (!this.diffPrefs) return;
+ await this.userService.updateDiffPreference(this.diffPrefs);
+ this.hasUnsavedChanges = false;
}
/**
diff --git a/polygerrit-ui/app/elements/shared/gr-diff-preferences/gr-diff-preferences_test.ts b/polygerrit-ui/app/elements/shared/gr-diff-preferences/gr-diff-preferences_test.ts
index 6c1404e..41ac3e3 100644
--- a/polygerrit-ui/app/elements/shared/gr-diff-preferences/gr-diff-preferences_test.ts
+++ b/polygerrit-ui/app/elements/shared/gr-diff-preferences/gr-diff-preferences_test.ts
@@ -51,7 +51,6 @@
element = basicFixture.instantiate();
- await element.loadData();
await flush();
});
diff --git a/polygerrit-ui/app/elements/shared/gr-download-commands/gr-download-commands.ts b/polygerrit-ui/app/elements/shared/gr-download-commands/gr-download-commands.ts
index 8322682..cac3d59 100644
--- a/polygerrit-ui/app/elements/shared/gr-download-commands/gr-download-commands.ts
+++ b/polygerrit-ui/app/elements/shared/gr-download-commands/gr-download-commands.ts
@@ -17,6 +17,7 @@
import '@polymer/paper-tabs/paper-tab';
import '@polymer/paper-tabs/paper-tabs';
import '../gr-shell-command/gr-shell-command';
+import '../../../styles/gr-paper-styles';
import '../../../styles/shared-styles';
import {PolymerElement} from '@polymer/polymer/polymer-element';
import {htmlTemplate} from './gr-download-commands_html';
diff --git a/polygerrit-ui/app/elements/shared/gr-download-commands/gr-download-commands_html.ts b/polygerrit-ui/app/elements/shared/gr-download-commands/gr-download-commands_html.ts
index 5a75c13..f9c08ba 100644
--- a/polygerrit-ui/app/elements/shared/gr-download-commands/gr-download-commands_html.ts
+++ b/polygerrit-ui/app/elements/shared/gr-download-commands/gr-download-commands_html.ts
@@ -17,6 +17,9 @@
import {html} from '@polymer/polymer/lib/utils/html-tag';
export const htmlTemplate = html`
+ <style include="gr-paper-styles">
+ /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
+ </style>
<style include="shared-styles">
paper-tabs {
height: 3rem;
diff --git a/polygerrit-ui/app/elements/shared/gr-hovercard-account/gr-hovercard-account.ts b/polygerrit-ui/app/elements/shared/gr-hovercard-account/gr-hovercard-account.ts
index 6a34fbb..e2ebb14 100644
--- a/polygerrit-ui/app/elements/shared/gr-hovercard-account/gr-hovercard-account.ts
+++ b/polygerrit-ui/app/elements/shared/gr-hovercard-account/gr-hovercard-account.ts
@@ -226,7 +226,7 @@
return html`
<div class="status">
<span class="title">
- <iron-icon icon="gr-icons:calendar"></iron-icon>
+ <iron-icon icon="gr-icons:unavailable"></iron-icon>
Status:
</span>
<span class="value">${this.account.status}</span>
diff --git a/polygerrit-ui/app/elements/shared/gr-icons/gr-icons.ts b/polygerrit-ui/app/elements/shared/gr-icons/gr-icons.ts
index 1a6239f..2533e00 100644
--- a/polygerrit-ui/app/elements/shared/gr-icons/gr-icons.ts
+++ b/polygerrit-ui/app/elements/shared/gr-icons/gr-icons.ts
@@ -162,6 +162,8 @@
<g id="description"><path xmlns="http://www.w3.org/2000/svg" d="M0 0h24v24H0V0z" fill="none"/><path xmlns="http://www.w3.org/2000/svg" d="M8 16h8v2H8zm0-4h8v2H8zm6-10H6c-1.1 0-2 .9-2 2v16c0 1.1.89 2 1.99 2H18c1.1 0 2-.9 2-2V8l-6-6zm4 18H6V4h7v5h5v11z"/></g>
<!-- This SVG is a copy from material.io https://fonts.google.com/icons?selected=Material+Icons&icon.query=settings_backup_restore and 0.65 scale and 4 translate https://fonts.google.com/icons?selected=Material+Icons&icon.query=done-->
<g id="overridden"><path xmlns="http://www.w3.org/2000/svg" d="M0 0h24v24H0V0z" fill="none"/><path xmlns="http://www.w3.org/2000/svg" d="M12 15 zM2 4v6h6V8H5.09C6.47 5.61 9.04 4 12 4c4.42 0 8 3.58 8 8s-3.58 8-8 8-8-3.58-8-8H2c0 5.52 4.48 10 10.01 10C17.53 22 22 17.52 22 12S17.53 2 12.01 2C8.73 2 5.83 3.58 4 6.01V4H2z"/><path xmlns="http://www.w3.org/2000/svg" d="M9.85 14.53 7.12 11.8l-.91.91L9.85 16.35 17.65 8.55l-.91-.91L9.85 14.53z"/></g>
+ <!-- This SVG is a copy from material.io https://fonts.google.com/icons?selected=Material+Icons:event_busy -->
+ <g id="unavailable"><path d="M0 0h24v24H0z" fill="none"/><path d="M9.31 17l2.44-2.44L14.19 17l1.06-1.06-2.44-2.44 2.44-2.44L14.19 10l-2.44 2.44L9.31 10l-1.06 1.06 2.44 2.44-2.44 2.44L9.31 17zM19 3h-1V1h-2v2H8V1H6v2H5c-1.11 0-1.99.9-1.99 2L3 19c0 1.1.89 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm0 16H5V8h14v11z"/></g>
</defs>
</svg>
</iron-iconset-svg>`;
diff --git a/polygerrit-ui/app/elements/shared/gr-select/gr-select.ts b/polygerrit-ui/app/elements/shared/gr-select/gr-select.ts
index 571272d..47295ab 100644
--- a/polygerrit-ui/app/elements/shared/gr-select/gr-select.ts
+++ b/polygerrit-ui/app/elements/shared/gr-select/gr-select.ts
@@ -34,7 +34,7 @@
}
@property({type: String, notify: true})
- bindValue?: string | number;
+ bindValue?: string | number | boolean;
get nativeSelect() {
// gr-select is not a shadow component
diff --git a/polygerrit-ui/app/elements/shared/gr-textarea/gr-textarea.ts b/polygerrit-ui/app/elements/shared/gr-textarea/gr-textarea.ts
index 9e6b42a..ce1b282 100644
--- a/polygerrit-ui/app/elements/shared/gr-textarea/gr-textarea.ts
+++ b/polygerrit-ui/app/elements/shared/gr-textarea/gr-textarea.ts
@@ -32,6 +32,7 @@
ItemSelectedEvent,
} from '../gr-autocomplete-dropdown/gr-autocomplete-dropdown';
import {addShortcut, Key} from '../../../utils/dom-util';
+import {BindValueChangeEvent} from '../../../types/events';
const MAX_ITEMS_DROPDOWN = 10;
@@ -63,10 +64,6 @@
match: string;
}
-interface ValueChangeEvent {
- value: string;
-}
-
export interface GrTextarea {
$: {
textarea: IronAutogrowTextareaElement;
@@ -79,7 +76,6 @@
declare global {
interface HTMLElementEventMap {
'item-selected': CustomEvent<ItemSelectedEvent>;
- 'bind-value-changed': CustomEvent<ValueChangeEvent>;
}
}
@@ -316,7 +312,7 @@
* _handleKeydown used for key handling in the this.$.textarea AND all child
* autocomplete options.
*/
- _onValueChanged(e: CustomEvent<ValueChangeEvent>) {
+ _onValueChanged(e: BindValueChangeEvent) {
// Relay the event.
this.dispatchEvent(
new CustomEvent('bind-value-changed', {
diff --git a/polygerrit-ui/app/elements/shared/gr-tooltip-content/gr-tooltip-content.ts b/polygerrit-ui/app/elements/shared/gr-tooltip-content/gr-tooltip-content.ts
index 0585aec8..4315071 100644
--- a/polygerrit-ui/app/elements/shared/gr-tooltip-content/gr-tooltip-content.ts
+++ b/polygerrit-ui/app/elements/shared/gr-tooltip-content/gr-tooltip-content.ts
@@ -210,13 +210,9 @@
const left = rect.left - parentRect.left + (rect.width - boxRect.width) / 2;
const right = parentRect.width - left - boxRect.width;
if (left < 0) {
- tooltip.updateStyles({
- '--gr-tooltip-arrow-center-offset': `${left}px`,
- });
+ tooltip.arrowCenterOffset = `${left}px`;
} else if (right < 0) {
- tooltip.updateStyles({
- '--gr-tooltip-arrow-center-offset': `${-0.5 * right}px`,
- });
+ tooltip.arrowCenterOffset = `${-0.5 * right}px`;
}
tooltip.style.left = `${Math.max(0, left)}px`;
diff --git a/polygerrit-ui/app/elements/shared/gr-tooltip-content/gr-tooltip-content_test.js b/polygerrit-ui/app/elements/shared/gr-tooltip-content/gr-tooltip-content_test.js
index 8d3bbb0..3b81f46 100644
--- a/polygerrit-ui/app/elements/shared/gr-tooltip-content/gr-tooltip-content_test.js
+++ b/polygerrit-ui/app/elements/shared/gr-tooltip-content/gr-tooltip-content_test.js
@@ -25,11 +25,15 @@
function makeTooltip(tooltipRect, parentRect) {
return {
- getBoundingClientRect() { return tooltipRect; },
- updateStyles: sinon.stub(),
+ arrowCenterOffset: '0',
+ getBoundingClientRect() {
+ return tooltipRect;
+ },
style: {left: 0, top: 0},
parentElement: {
- getBoundingClientRect() { return parentRect; },
+ getBoundingClientRect() {
+ return parentRect;
+ },
},
};
}
@@ -66,12 +70,12 @@
{top: 0, left: 0, width: 1000});
element._positionTooltip(tooltip);
- assert.isFalse(tooltip.updateStyles.called);
+ assert.equal(tooltip.arrowCenterOffset, '0');
assert.equal(tooltip.style.left, '175px');
assert.equal(tooltip.style.top, '100px');
});
- test('left side position', () => {
+ test('left side position', async () => {
sinon.stub(element, 'getBoundingClientRect').callsFake(() => {
return {top: 100, left: 10, width: 50};
});
@@ -80,10 +84,8 @@
{top: 0, left: 0, width: 1000});
element._positionTooltip(tooltip);
- assert.isTrue(tooltip.updateStyles.called);
- const offset = tooltip.updateStyles
- .lastCall.args[0]['--gr-tooltip-arrow-center-offset'];
- assert.isBelow(parseFloat(offset.replace(/px$/, '')), 0);
+ await element.updateComplete;
+ assert.isBelow(parseFloat(tooltip.arrowCenterOffset.replace(/px$/, '')), 0);
assert.equal(tooltip.style.left, '0px');
assert.equal(tooltip.style.top, '100px');
});
@@ -97,10 +99,7 @@
{top: 0, left: 0, width: 1000});
element._positionTooltip(tooltip);
- assert.isTrue(tooltip.updateStyles.called);
- const offset = tooltip.updateStyles
- .lastCall.args[0]['--gr-tooltip-arrow-center-offset'];
- assert.isAbove(parseFloat(offset.replace(/px$/, '')), 0);
+ assert.isAbove(parseFloat(tooltip.arrowCenterOffset.replace(/px$/, '')), 0);
assert.equal(tooltip.style.left, '915px');
assert.equal(tooltip.style.top, '100px');
});
@@ -115,19 +114,16 @@
element.positionBelow = true;
element._positionTooltip(tooltip);
- assert.isTrue(tooltip.updateStyles.called);
- const offset = tooltip.updateStyles
- .lastCall.args[0]['--gr-tooltip-arrow-center-offset'];
- assert.isAbove(parseFloat(offset.replace(/px$/, '')), 0);
+ assert.isAbove(parseFloat(tooltip.arrowCenterOffset.replace(/px$/, '')), 0);
assert.equal(tooltip.style.left, '915px');
assert.equal(tooltip.style.top, '157.2px');
});
test('hides tooltip when detached', async () => {
- sinon.stub(element, '_handleHideTooltip');
+ const handleHideTooltipStub = sinon.stub(element, '_handleHideTooltip');
element.remove();
await element.updateComplete;
- assert.isTrue(element._handleHideTooltip.called);
+ assert.isTrue(handleHideTooltipStub.called);
});
test('sets up listeners when has-tooltip is changed', async () => {
diff --git a/polygerrit-ui/app/elements/shared/gr-tooltip/gr-tooltip.ts b/polygerrit-ui/app/elements/shared/gr-tooltip/gr-tooltip.ts
index cab05b4..0e41891 100644
--- a/polygerrit-ui/app/elements/shared/gr-tooltip/gr-tooltip.ts
+++ b/polygerrit-ui/app/elements/shared/gr-tooltip/gr-tooltip.ts
@@ -14,14 +14,11 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-import '../../../styles/shared-styles';
-import {PolymerElement} from '@polymer/polymer/polymer-element';
-import {htmlTemplate} from './gr-tooltip_html';
-import {customElement, property, observe} from '@polymer/decorators';
-export interface GrTooltip {
- $: {};
-}
+import {sharedStyles} from '../../../styles/shared-styles';
+import {LitElement, css, html} from 'lit';
+import {customElement, property} from 'lit/decorators';
+import {styleMap} from 'lit/directives/style-map';
declare global {
interface HTMLElementTagNameMap {
@@ -30,22 +27,78 @@
}
@customElement('gr-tooltip')
-export class GrTooltip extends PolymerElement {
- static get template() {
- return htmlTemplate;
- }
-
+export class GrTooltip extends LitElement {
@property({type: String})
text = '';
@property({type: String})
maxWidth = '';
- @property({type: Boolean, reflectToAttribute: true})
+ @property({type: String})
+ arrowCenterOffset = '0';
+
+ @property({type: Boolean, reflect: true, attribute: 'position-below'})
positionBelow = false;
- @observe('maxWidth')
- _updateWidth(maxWidth: string) {
- this.updateStyles({'--tooltip-max-width': maxWidth});
+ static override get styles() {
+ return [
+ sharedStyles,
+ css`
+ :host {
+ --gr-tooltip-arrow-size: 0.5em;
+
+ background-color: var(--tooltip-background-color);
+ box-shadow: var(--elevation-level-2);
+ color: var(--tooltip-text-color);
+ font-size: var(--font-size-small);
+ position: absolute;
+ z-index: 1000;
+ }
+ :host .tooltip {
+ padding: var(--spacing-m) var(--spacing-l);
+ }
+ :host .arrowPositionBelow,
+ :host([position-below]) .arrowPositionAbove {
+ display: none;
+ }
+ :host([position-below]) .arrowPositionBelow {
+ display: initial;
+ }
+ .arrow {
+ border-left: var(--gr-tooltip-arrow-size) solid transparent;
+ border-right: var(--gr-tooltip-arrow-size) solid transparent;
+ height: 0;
+ position: absolute;
+ left: calc(50% - var(--gr-tooltip-arrow-size));
+ width: 0;
+ }
+ .arrowPositionAbove {
+ border-top: var(--gr-tooltip-arrow-size) solid
+ var(--tooltip-background-color);
+ bottom: calc(-1 * var(--gr-tooltip-arrow-size));
+ }
+ .arrowPositionBelow {
+ border-bottom: var(--gr-tooltip-arrow-size) solid
+ var(--tooltip-background-color);
+ top: calc(-1 * var(--gr-tooltip-arrow-size));
+ }
+ `,
+ ];
+ }
+
+ override render() {
+ this.style.maxWidth = this.maxWidth;
+
+ return html` <div class="tooltip">
+ <i
+ class="arrowPositionBelow arrow"
+ style="${styleMap({marginLeft: this.arrowCenterOffset})}"
+ ></i>
+ ${this.text}
+ <i
+ class="arrowPositionAbove arrow"
+ style="${styleMap({marginLeft: this.arrowCenterOffset})}"
+ ></i>
+ </div>`;
}
}
diff --git a/polygerrit-ui/app/elements/shared/gr-tooltip/gr-tooltip_html.ts b/polygerrit-ui/app/elements/shared/gr-tooltip/gr-tooltip_html.ts
deleted file mode 100644
index d59a6c3..0000000
--- a/polygerrit-ui/app/elements/shared/gr-tooltip/gr-tooltip_html.ts
+++ /dev/null
@@ -1,68 +0,0 @@
-/**
- * @license
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-import {html} from '@polymer/polymer/lib/utils/html-tag';
-
-export const htmlTemplate = html`
- <style include="shared-styles">
- :host {
- --gr-tooltip-arrow-size: 0.5em;
- --gr-tooltip-arrow-center-offset: 0;
-
- background-color: var(--tooltip-background-color);
- box-shadow: var(--elevation-level-2);
- color: var(--tooltip-text-color);
- font-size: var(--font-size-small);
- position: absolute;
- z-index: 1000;
- max-width: var(--tooltip-max-width);
- }
- :host .tooltip {
- padding: var(--spacing-m) var(--spacing-l);
- }
- :host .arrowPositionBelow,
- :host([position-below]) .arrowPositionAbove {
- display: none;
- }
- :host([position-below]) .arrowPositionBelow {
- display: initial;
- }
- .arrow {
- border-left: var(--gr-tooltip-arrow-size) solid transparent;
- border-right: var(--gr-tooltip-arrow-size) solid transparent;
- height: 0;
- position: absolute;
- left: calc(50% - var(--gr-tooltip-arrow-size));
- margin-left: var(--gr-tooltip-arrow-center-offset);
- width: 0;
- }
- .arrowPositionAbove {
- border-top: var(--gr-tooltip-arrow-size) solid
- var(--tooltip-background-color);
- bottom: calc(-1 * var(--gr-tooltip-arrow-size));
- }
- .arrowPositionBelow {
- border-bottom: var(--gr-tooltip-arrow-size) solid
- var(--tooltip-background-color);
- top: calc(-1 * var(--gr-tooltip-arrow-size));
- }
- </style>
- <div class="tooltip">
- <i class="arrowPositionBelow arrow"></i>
- [[text]]
- <i class="arrowPositionAbove arrow"></i>
- </div>
-`;
diff --git a/polygerrit-ui/app/elements/shared/gr-tooltip/gr-tooltip_test.ts b/polygerrit-ui/app/elements/shared/gr-tooltip/gr-tooltip_test.ts
index 8b44047..b693a9e 100644
--- a/polygerrit-ui/app/elements/shared/gr-tooltip/gr-tooltip_test.ts
+++ b/polygerrit-ui/app/elements/shared/gr-tooltip/gr-tooltip_test.ts
@@ -17,49 +17,45 @@
import '../../../test/common-test-setup-karma';
import './gr-tooltip';
-import {html} from '@polymer/polymer/lib/utils/html-tag';
import {GrTooltip} from './gr-tooltip';
+import {queryAndAssert} from '../../../test/test-utils';
-const basicFixture = fixtureFromTemplate(html` <gr-tooltip> </gr-tooltip> `);
+const basicFixture = fixtureFromElement('gr-tooltip');
suite('gr-tooltip tests', () => {
let element: GrTooltip;
- setup(() => {
+
+ setup(async () => {
element = basicFixture.instantiate() as GrTooltip;
+ await element.updateComplete;
});
- test('max-width is respected if set', () => {
+ test('max-width is respected if set', async () => {
element.text =
'Lorem ipsum dolor sit amet, consectetur adipiscing elit' +
', sed do eiusmod tempor incididunt ut labore et dolore magna aliqua';
element.maxWidth = '50px';
+ await element.updateComplete;
assert.equal(getComputedStyle(element).width, '50px');
});
- test('the correct arrow is displayed', () => {
+ test('the correct arrow is displayed', async () => {
assert.equal(
- getComputedStyle(
- element.shadowRoot!.querySelector('.arrowPositionBelow')!
- ).display,
+ getComputedStyle(queryAndAssert(element, '.arrowPositionBelow')!).display,
'none'
);
assert.notEqual(
- getComputedStyle(
- element.shadowRoot!.querySelector('.arrowPositionAbove')!
- ).display,
+ getComputedStyle(queryAndAssert(element, '.arrowPositionAbove')!).display,
'none'
);
element.positionBelow = true;
+ await element.updateComplete;
assert.notEqual(
- getComputedStyle(
- element.shadowRoot!.querySelector('.arrowPositionBelow')!
- ).display,
+ getComputedStyle(queryAndAssert(element, '.arrowPositionBelow')!).display,
'none'
);
assert.equal(
- getComputedStyle(
- element.shadowRoot!.querySelector('.arrowPositionAbove')!
- ).display,
+ getComputedStyle(queryAndAssert(element, '.arrowPositionAbove')!).display,
'none'
);
});
diff --git a/polygerrit-ui/app/mixins/hovercard-mixin/hovercard-mixin_test.ts b/polygerrit-ui/app/mixins/hovercard-mixin/hovercard-mixin_test.ts
index dd86b38..e6b63e6 100644
--- a/polygerrit-ui/app/mixins/hovercard-mixin/hovercard-mixin_test.ts
+++ b/polygerrit-ui/app/mixins/hovercard-mixin/hovercard-mixin_test.ts
@@ -47,7 +47,7 @@
let element: HovercardMixinTest;
let button: HTMLElement;
- let testPromise: MockPromise;
+ let testPromise: MockPromise<void>;
setup(() => {
testPromise = mockPromise();
diff --git a/polygerrit-ui/app/services/flags/flags.ts b/polygerrit-ui/app/services/flags/flags.ts
index 2839874..863f95f 100644
--- a/polygerrit-ui/app/services/flags/flags.ts
+++ b/polygerrit-ui/app/services/flags/flags.ts
@@ -25,7 +25,6 @@
*/
export enum KnownExperimentId {
NEW_IMAGE_DIFF_UI = 'UiFeature__new_image_diff_ui',
- TOKEN_HIGHLIGHTING = 'UiFeature__token_highlighting',
CHECKS_DEVELOPER = 'UiFeature__checks_developer',
SUBMIT_REQUIREMENTS_UI = 'UiFeature__submit_requirements_ui',
}
diff --git a/polygerrit-ui/app/services/user/user-model.ts b/polygerrit-ui/app/services/user/user-model.ts
index 6d31f6c..000887c 100644
--- a/polygerrit-ui/app/services/user/user-model.ts
+++ b/polygerrit-ui/app/services/user/user-model.ts
@@ -19,8 +19,9 @@
import {map, distinctUntilChanged} from 'rxjs/operators';
import {
createDefaultPreferences,
- DiffViewMode,
+ createDefaultDiffPrefs,
} from '../../constants/constants';
+import {DiffPreferencesInfo, DiffViewMode} from '../../api/diff';
interface UserState {
/**
@@ -28,10 +29,12 @@
*/
account?: AccountDetailInfo;
preferences: PreferencesInfo;
+ diffPreferences: DiffPreferencesInfo;
}
const initialState: UserState = {
preferences: createDefaultPreferences(),
+ diffPreferences: createDefaultDiffPrefs(),
};
// Mutable for testing
@@ -62,6 +65,11 @@
privateState$.next({...current, preferences});
}
+export function updateDiffPreferences(diffPreferences: DiffPreferencesInfo) {
+ const current = privateState$.getValue();
+ privateState$.next({...current, diffPreferences});
+}
+
export const account$ = userState$.pipe(
map(userState => userState.account),
distinctUntilChanged()
@@ -72,6 +80,11 @@
distinctUntilChanged()
);
+export const diffPreferences$ = userState$.pipe(
+ map(userState => userState.diffPreferences),
+ distinctUntilChanged()
+);
+
export const preferenceDiffViewMode$ = preferences$.pipe(
map(preference => preference.diff_view ?? DiffViewMode.SIDE_BY_SIDE),
distinctUntilChanged()
@@ -82,6 +95,11 @@
distinctUntilChanged()
);
+export const sizeBarInChangeTable$ = preferences$.pipe(
+ map(prefs => !!prefs?.size_bar_in_change_table),
+ distinctUntilChanged()
+);
+
export const disableShortcuts$ = preferences$.pipe(
map(preferences => preferences?.disable_keyboard_shortcuts ?? false),
distinctUntilChanged()
diff --git a/polygerrit-ui/app/services/user/user-service.ts b/polygerrit-ui/app/services/user/user-service.ts
index 0588a4a..d08da8b 100644
--- a/polygerrit-ui/app/services/user/user-service.ts
+++ b/polygerrit-ui/app/services/user/user-service.ts
@@ -16,10 +16,19 @@
*/
import {AccountDetailInfo, PreferencesInfo} from '../../types/common';
import {from, of} from 'rxjs';
-import {account$, updateAccount, updatePreferences} from './user-model';
+import {
+ account$,
+ updateAccount,
+ updatePreferences,
+ updateDiffPreferences,
+} from './user-model';
import {switchMap} from 'rxjs/operators';
-import {createDefaultPreferences} from '../../constants/constants';
+import {
+ createDefaultPreferences,
+ createDefaultDiffPrefs,
+} from '../../constants/constants';
import {RestApiService} from '../gr-rest-api/gr-rest-api';
+import {DiffPreferencesInfo} from '../../types/diff';
export class UserService {
constructor(readonly restApiService: RestApiService) {
@@ -38,6 +47,16 @@
.subscribe((preferences?: PreferencesInfo) => {
updatePreferences(preferences ?? createDefaultPreferences());
});
+ account$
+ .pipe(
+ switchMap(account => {
+ if (!account) return of(createDefaultDiffPrefs());
+ return from(this.restApiService.getDiffPreferences());
+ })
+ )
+ .subscribe((diffPrefs?: DiffPreferencesInfo) => {
+ updateDiffPreferences(diffPrefs ?? createDefaultDiffPrefs());
+ });
}
updatePreferences(prefs: Partial<PreferencesInfo>) {
@@ -48,4 +67,23 @@
updatePreferences(newPrefs);
});
}
+
+ updateDiffPreference(diffPrefs: DiffPreferencesInfo) {
+ return this.restApiService
+ .saveDiffPreferences(diffPrefs)
+ .then((response: Response) => {
+ this.restApiService.getResponseObject(response).then(obj => {
+ const newPrefs = obj as unknown as DiffPreferencesInfo;
+ if (!newPrefs) return;
+ updateDiffPreferences(newPrefs);
+ });
+ });
+ }
+
+ getDiffPreferences() {
+ return this.restApiService.getDiffPreferences().then(prefs => {
+ if (!prefs) return;
+ updateDiffPreferences(prefs);
+ });
+ }
}
diff --git a/polygerrit-ui/app/styles/gr-paper-styles.ts b/polygerrit-ui/app/styles/gr-paper-styles.ts
new file mode 100644
index 0000000..1ef7124
--- /dev/null
+++ b/polygerrit-ui/app/styles/gr-paper-styles.ts
@@ -0,0 +1,60 @@
+/**
+ * @license
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import {css} from 'lit';
+
+export const paperStyles = css`
+ paper-toggle-button {
+ --paper-toggle-button-checked-bar-color: var(--link-color);
+ --paper-toggle-button-checked-button-color: var(--link-color);
+ }
+ paper-tabs {
+ font-size: var(--font-size-h3);
+ font-weight: var(--font-weight-h3);
+ line-height: var(--line-height-h3);
+ --paper-font-common-base: {
+ font-family: var(--header-font-family);
+ -webkit-font-smoothing: initial;
+ }
+ --paper-tab-content: {
+ margin-bottom: var(--spacing-s);
+ }
+ --paper-tab-content-focused: {
+ /* paper-tabs uses 700 here, which can look awkward */
+ font-weight: var(--font-weight-h3);
+ background: var(--gray-background-focus);
+ }
+ --paper-tab-content-unselected: {
+ /* paper-tabs uses 0.8 here, but we want to control the color directly */
+ opacity: 1;
+ color: var(--deemphasized-text-color);
+ }
+ }
+ paper-tab:focus {
+ padding-left: 0px;
+ padding-right: 0px;
+ }
+`;
+
+const $_documentContainer = document.createElement('template');
+$_documentContainer.innerHTML = `<dom-module id="gr-paper-styles">
+ <template>
+ <style>
+ ${paperStyles.cssText}
+ </style>
+ </template>
+</dom-module>`;
+document.head.appendChild($_documentContainer.content);
diff --git a/polygerrit-ui/app/styles/shared-styles.ts b/polygerrit-ui/app/styles/shared-styles.ts
index 98f6eb2..e99cf27 100644
--- a/polygerrit-ui/app/styles/shared-styles.ts
+++ b/polygerrit-ui/app/styles/shared-styles.ts
@@ -189,36 +189,6 @@
.separator.transparent {
border-color: transparent;
}
- paper-toggle-button {
- --paper-toggle-button-checked-bar-color: var(--link-color);
- --paper-toggle-button-checked-button-color: var(--link-color);
- }
- paper-tabs {
- font-size: var(--font-size-h3);
- font-weight: var(--font-weight-h3);
- line-height: var(--line-height-h3);
- --paper-font-common-base: {
- font-family: var(--header-font-family);
- -webkit-font-smoothing: initial;
- }
- --paper-tab-content: {
- margin-bottom: var(--spacing-s);
- }
- --paper-tab-content-focused: {
- /* paper-tabs uses 700 here, which can look awkward */
- font-weight: var(--font-weight-h3);
- background: var(--gray-background-focus);
- }
- --paper-tab-content-unselected: {
- /* paper-tabs uses 0.8 here, but we want to control the color directly */
- opacity: 1;
- color: var(--deemphasized-text-color);
- }
- }
- paper-tab:focus {
- padding-left: 0px;
- padding-right: 0px;
- }
iron-autogrow-textarea {
/** This is needed for firefox */
--iron-autogrow-textarea_-_white-space: pre-wrap;
diff --git a/polygerrit-ui/app/styles/themes/dark-theme.ts b/polygerrit-ui/app/styles/themes/dark-theme.ts
index a24a666..05c6b7d 100644
--- a/polygerrit-ui/app/styles/themes/dark-theme.ts
+++ b/polygerrit-ui/app/styles/themes/dark-theme.ts
@@ -249,10 +249,3 @@
export function applyTheme() {
document.head.appendChild(getStyleEl());
}
-
-export function removeTheme() {
- const darkThemeEls = document.head.querySelectorAll('#dark-theme');
- if (darkThemeEls.length) {
- darkThemeEls.forEach(darkThemeEl => darkThemeEl.remove());
- }
-}
diff --git a/polygerrit-ui/app/styles/themes/dark-theme_test.ts b/polygerrit-ui/app/styles/themes/dark-theme_test.ts
deleted file mode 100644
index 16e609e..0000000
--- a/polygerrit-ui/app/styles/themes/dark-theme_test.ts
+++ /dev/null
@@ -1,28 +0,0 @@
-/**
- * @license
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-import '../../test/common-test-setup-karma';
-import {applyTheme, removeTheme} from './dark-theme';
-
-suite('dark-theme test', () => {
- test('apply and remove theme', () => {
- applyTheme();
- assert.equal(document.head.querySelectorAll('#dark-theme').length, 1);
- removeTheme();
- assert.isEmpty(document.head.querySelectorAll('#dark-theme'));
- });
-});
diff --git a/polygerrit-ui/app/test/common-test-setup.ts b/polygerrit-ui/app/test/common-test-setup.ts
index 949c268..bd5504a 100644
--- a/polygerrit-ui/app/test/common-test-setup.ts
+++ b/polygerrit-ui/app/test/common-test-setup.ts
@@ -30,6 +30,7 @@
registerTestCleanup,
addIronOverlayBackdropStyleEl,
removeIronOverlayBackdropStyleEl,
+ removeThemeStyles,
} from './test-utils';
import {safeTypesBridge} from '../utils/safe-types-util';
import {_testOnly_initGerritPluginApi} from '../elements/shared/gr-js-api-interface/gr-gerrit';
@@ -197,6 +198,7 @@
cleanupTestUtils();
checkGlobalSpace();
removeIronOverlayBackdropStyleEl();
+ removeThemeStyles();
cancelAllTasks();
cleanUpStorage();
// Reset state
diff --git a/polygerrit-ui/app/test/test-data-generators.ts b/polygerrit-ui/app/test/test-data-generators.ts
index 351cc13..dd56ce2 100644
--- a/polygerrit-ui/app/test/test-data-generators.ts
+++ b/polygerrit-ui/app/test/test-data-generators.ts
@@ -230,7 +230,10 @@
};
}
-export function createRevision(patchSetNum = 1): RevisionInfo {
+export function createRevision(
+ patchSetNum = 1,
+ description = ''
+): RevisionInfo {
return {
_number: patchSetNum as PatchSetNum,
commit: createCommit(),
@@ -238,13 +241,14 @@
kind: RevisionKind.REWORK,
ref: 'refs/changes/5/6/1' as GitRef,
uploader: createAccountWithId(),
+ description,
};
}
-export function createEditRevision(): EditRevisionInfo {
+export function createEditRevision(basePatchNum = 1): EditRevisionInfo {
return {
_number: EditPatchSetNum,
- basePatchNum: 1 as BasePatchSetNum,
+ basePatchNum: basePatchNum as BasePatchSetNum,
commit: createCommit(),
};
}
@@ -270,7 +274,7 @@
[revisionId: string]: RevisionInfo;
} {
const revisions: {[revisionId: string]: RevisionInfo} = {};
- const revisionDate = TEST_CHANGE_CREATED;
+ let revisionDate = TEST_CHANGE_CREATED;
const revisionIdStart = 1; // The same as getCurrentRevision
for (let i = 0; i < count; i++) {
const revisionId = (i + revisionIdStart).toString(16);
@@ -281,6 +285,7 @@
};
revisions[revisionId] = revision;
// advance 1 day
+ revisionDate = new Date(revisionDate);
revisionDate.setDate(revisionDate.getDate() + 1);
}
return revisions;
@@ -294,12 +299,13 @@
export function createChangeMessages(count: number): ChangeMessageInfo[] {
const messageIdStart = 1000;
const messages: ChangeMessageInfo[] = [];
- const messageDate = TEST_CHANGE_CREATED;
+ let messageDate = TEST_CHANGE_CREATED;
for (let i = 0; i < count; i++) {
messages.push({
...createChangeMessageInfo((i + messageIdStart).toString(16)),
date: dateToTimestamp(messageDate),
});
+ messageDate = new Date(messageDate);
messageDate.setDate(messageDate.getDate() + 1);
}
return messages;
diff --git a/polygerrit-ui/app/test/test-utils.ts b/polygerrit-ui/app/test/test-utils.ts
index 49ecac8..4a513f8 100644
--- a/polygerrit-ui/app/test/test-utils.ts
+++ b/polygerrit-ui/app/test/test-utils.ts
@@ -26,20 +26,21 @@
import {CommentsService} from '../services/comments/comments-service';
import {UserService} from '../services/user/user-service';
import {ShortcutsService} from '../services/shortcuts/shortcuts-service';
+import {queryAndAssert, query} from '../utils/common-util';
export {query, queryAll, queryAndAssert} from '../utils/common-util';
-export interface MockPromise extends Promise<unknown> {
- resolve: (value?: unknown) => void;
+export interface MockPromise<T> extends Promise<T> {
+ resolve: (value?: T) => void;
}
-export const mockPromise = () => {
- let res: (value?: unknown) => void;
- const promise: MockPromise = new Promise(resolve => {
+export function mockPromise<T = unknown>(): MockPromise<T> {
+ let res: (value?: T) => void;
+ const promise: MockPromise<T> = new Promise<T | undefined>(resolve => {
res = resolve;
- }) as MockPromise;
+ }) as MockPromise<T>;
promise.resolve = res!;
return promise;
-};
+}
export function isHidden(el: Element | undefined | null) {
if (!el) return true;
@@ -160,22 +161,40 @@
el.parentNode?.removeChild(el);
}
+export function removeThemeStyles() {
+ // Do not remove the light theme, because it is only added once statically,
+ // not once per gr-app instantiation.
+ // document.head.querySelector('#light-theme')?.remove();
+ document.head.querySelector('#dark-theme')?.remove();
+}
+
+export async function waitQueryAndAssert<E extends Element = Element>(
+ el: Element | null | undefined,
+ selector: string
+): Promise<E> {
+ await waitUntil(
+ () => !!query<E>(el, selector),
+ `The element '${selector}' did not appear in the DOM within 1000 ms.`
+ );
+ return queryAndAssert<E>(el, selector);
+}
+
export function waitUntil(
predicate: () => boolean,
- maxMillis = 100
+ message = 'The waitUntil() predicate is still false after 1000 ms.'
): Promise<void> {
const start = Date.now();
- let sleep = 1;
+ let sleep = 0;
return new Promise((resolve, reject) => {
const waiter = () => {
if (predicate()) {
return resolve();
}
- if (Date.now() - start >= maxMillis) {
- return reject(new Error('Took to long to waitUntil'));
+ if (Date.now() - start >= 1000) {
+ return reject(new Error(message));
}
setTimeout(waiter, sleep);
- sleep *= 2;
+ sleep = sleep === 0 ? 1 : sleep * 4;
};
waiter();
});
diff --git a/polygerrit-ui/app/types/diff.ts b/polygerrit-ui/app/types/diff.ts
index 223f290..8146eb3 100644
--- a/polygerrit-ui/app/types/diff.ts
+++ b/polygerrit-ui/app/types/diff.ts
@@ -98,7 +98,7 @@
export interface DiffPreferencesInfo extends DiffPreferenceInfoApi {
expand_all_comments?: boolean;
- cursor_blink_rate: number;
+ cursor_blink_rate?: number;
manual_review?: boolean;
retain_header?: boolean;
skip_deleted?: boolean;
diff --git a/polygerrit-ui/app/types/events.ts b/polygerrit-ui/app/types/events.ts
index b6376c4..f467cf6 100644
--- a/polygerrit-ui/app/types/events.ts
+++ b/polygerrit-ui/app/types/events.ts
@@ -22,6 +22,7 @@
import {ChangeMessage} from '../elements/change/gr-message/gr-message';
export enum EventType {
+ BIND_VALUE_CHANGED = 'bind-value-changed',
CHANGE = 'change',
CHANGED = 'changed',
CHANGE_MESSAGE_DELETED = 'change-message-deleted',
@@ -56,6 +57,8 @@
declare global {
interface HTMLElementEventMap {
/* prettier-ignore */
+ 'bind-value-changed': BindValueChangeEvent;
+ /* prettier-ignore */
'change': ChangeEvent;
/* prettier-ignore */
'changed': ChangedEvent;
@@ -102,6 +105,11 @@
}
}
+export interface BindValueChangeEventDetail {
+ value: string;
+}
+export type BindValueChangeEvent = CustomEvent<BindValueChangeEventDetail>;
+
export type ChangeEvent = InputEvent;
export type ChangedEvent = CustomEvent<string>;
diff --git a/polygerrit-ui/app/utils/dom-util_test.ts b/polygerrit-ui/app/utils/dom-util_test.ts
index 9dd5be2..e139805 100644
--- a/polygerrit-ui/app/utils/dom-util_test.ts
+++ b/polygerrit-ui/app/utils/dom-util_test.ts
@@ -28,22 +28,28 @@
import {PolymerElement} from '@polymer/polymer/polymer-element';
import {html} from '@polymer/polymer/lib/utils/html-tag';
import * as MockInteractions from '@polymer/iron-test-helpers/mock-interactions';
-import {queryAndAssert} from '../test/test-utils';
+import {mockPromise, queryAndAssert} from '../test/test-utils';
-async function keyEventOn(
+/**
+ * You might think that instead of passing in the callback with assertions as a
+ * parameter that you could as well just `await keyEventOn()` and *then* run
+ * your assertions. But at that point the event is not "hot" anymore, so most
+ * likely you want to assert stuff about the event within the callback
+ * parameter.
+ */
+function keyEventOn(
el: HTMLElement,
callback: (e: KeyboardEvent) => void,
keyCode = 75,
key = 'k'
): Promise<KeyboardEvent> {
- let resolve: (e: KeyboardEvent) => void;
- const promise = new Promise<KeyboardEvent>(r => (resolve = r));
+ const promise = mockPromise<KeyboardEvent>();
el.addEventListener('keydown', (e: KeyboardEvent) => {
callback(e);
- resolve(e);
+ promise.resolve(e);
});
MockInteractions.keyDownOn(el, keyCode, null, key);
- return await promise;
+ return promise;
}
class TestEle extends PolymerElement {
diff --git a/polygerrit-ui/app/utils/label-util.ts b/polygerrit-ui/app/utils/label-util.ts
index b95fa4d..1a48a7b 100644
--- a/polygerrit-ui/app/utils/label-util.ts
+++ b/polygerrit-ui/app/utils/label-util.ts
@@ -15,6 +15,7 @@
* limitations under the License.
*/
import {
+ ChangeInfo,
isQuickLabelInfo,
SubmitRequirementResultInfo,
SubmitRequirementStatus,
@@ -28,6 +29,7 @@
LabelNameToInfoMap,
VotingRangeInfo,
} from '../types/common';
+import {ParsedChangeInfo} from '../types/types';
import {assertNever, unique} from './common-util';
// Name of the standard Code-Review label.
@@ -243,8 +245,30 @@
}
}
+/**
+ * Show only applicable.
+ * If there are only legacy requirements, show all legacy requirements.
+ * If there is at least one non-legacy requirement, filter legacy requirements.
+ */
+export function getRequirements(change?: ParsedChangeInfo | ChangeInfo) {
+ let submit_requirements = (change?.submit_requirements ?? []).filter(
+ req => req.status !== SubmitRequirementStatus.NOT_APPLICABLE
+ );
+
+ const hasNonLegacyRequirements = submit_requirements.some(
+ req => req.is_legacy === false
+ );
+ if (hasNonLegacyRequirements) {
+ submit_requirements = submit_requirements.filter(
+ req => req.is_legacy === false
+ );
+ }
+
+ return submit_requirements;
+}
+
// TODO(milutin): This may be temporary for demo purposes
-const PRIORITY_REQUIREMENTS_ORDER: string[] = [
+export const PRIORITY_REQUIREMENTS_ORDER: string[] = [
StandardLabels.CODE_REVIEW,
StandardLabels.CODE_OWNERS,
StandardLabels.PRESUBMIT_VERIFIED,
@@ -263,3 +287,14 @@
);
return priorityRequirementList.concat(nonPriorityRequirements);
}
+
+export function getTriggerVotes(change?: ParsedChangeInfo | ChangeInfo) {
+ const allLabels = Object.keys(change?.labels ?? {});
+ const submitReqs = getRequirements(change);
+ const labelAssociatedWithSubmitReqs = submitReqs
+ .flatMap(req => extractAssociatedLabels(req))
+ .filter(unique);
+ return allLabels.filter(
+ label => !labelAssociatedWithSubmitReqs.includes(label)
+ );
+}
diff --git a/polygerrit-ui/app/utils/label-util_test.ts b/polygerrit-ui/app/utils/label-util_test.ts
index 2d59294..142c607 100644
--- a/polygerrit-ui/app/utils/label-util_test.ts
+++ b/polygerrit-ui/app/utils/label-util_test.ts
@@ -24,6 +24,8 @@
getRepresentativeValue,
getVotingRange,
getVotingRangeOrDefault,
+ getRequirements,
+ getTriggerVotes,
hasNeutralStatus,
labelCompare,
LabelStatus,
@@ -38,9 +40,15 @@
} from '../types/common';
import {
createAccountWithEmail,
+ createChange,
createSubmitRequirementExpressionInfo,
createSubmitRequirementResultInfo,
+ createDetailedLabelInfo,
} from '../test/test-data-generators';
+import {
+ SubmitRequirementResultInfo,
+ SubmitRequirementStatus,
+} from '../api/rest-api';
const VALUES_0 = {
'0': 'neutral',
@@ -270,4 +278,79 @@
assert.deepEqual(labels, ['Verified', 'Build-cop-override']);
});
});
+
+ suite('getRequirements()', () => {
+ function createChangeInfoWith(
+ submit_requirements: SubmitRequirementResultInfo[]
+ ) {
+ return {
+ ...createChange(),
+ submit_requirements,
+ };
+ }
+ test('only legacy', () => {
+ const requirement = {
+ ...createSubmitRequirementResultInfo(),
+ is_legacy: true,
+ };
+ const change = createChangeInfoWith([requirement]);
+ assert.deepEqual(getRequirements(change), [requirement]);
+ });
+ test('legacy and non-legacy - filter legacy', () => {
+ const requirement = {
+ ...createSubmitRequirementResultInfo(),
+ is_legacy: true,
+ };
+ const requirement2 = {
+ ...createSubmitRequirementResultInfo(),
+ is_legacy: false,
+ };
+ const change = createChangeInfoWith([requirement, requirement2]);
+ assert.deepEqual(getRequirements(change), [requirement2]);
+ });
+ test('filter not applicable', () => {
+ const requirement = {
+ ...createSubmitRequirementResultInfo(),
+ is_legacy: true,
+ };
+ const requirement2 = {
+ ...createSubmitRequirementResultInfo(),
+ status: SubmitRequirementStatus.NOT_APPLICABLE,
+ };
+ const change = createChangeInfoWith([requirement, requirement2]);
+ assert.deepEqual(getRequirements(change), [requirement]);
+ });
+ });
+
+ suite('getTriggerVotes()', () => {
+ test('no requirements', () => {
+ const triggerVote = 'Trigger-Vote';
+ const change = {
+ ...createChange(),
+ labels: {
+ [triggerVote]: createDetailedLabelInfo(),
+ },
+ };
+ assert.deepEqual(getTriggerVotes(change), [triggerVote]);
+ });
+ test('no trigger votes, all labels associated with sub requirement', () => {
+ const triggerVote = 'Trigger-Vote';
+ const change = {
+ ...createChange(),
+ submit_requirements: [
+ {
+ ...createSubmitRequirementResultInfo(),
+ submittability_expression_result: {
+ ...createSubmitRequirementExpressionInfo(),
+ expression: `label:${triggerVote}=MAX`,
+ },
+ },
+ ],
+ labels: {
+ [triggerVote]: createDetailedLabelInfo(),
+ },
+ };
+ assert.deepEqual(getTriggerVotes(change), []);
+ });
+ });
});
diff --git a/polygerrit-ui/app/utils/patch-set-util.ts b/polygerrit-ui/app/utils/patch-set-util.ts
index ce5e5a4..ee4ed8b 100644
--- a/polygerrit-ui/app/utils/patch-set-util.ts
+++ b/polygerrit-ui/app/utils/patch-set-util.ts
@@ -95,7 +95,7 @@
* @return The correspondent revision obj from {revisions}
*/
export function getRevisionByPatchNum(
- revisions: RevisionInfo[],
+ revisions: (RevisionInfo | EditRevisionInfo)[],
patchNum: PatchSetNum
) {
for (const rev of revisions) {
@@ -309,10 +309,11 @@
*/
export function findSortedIndex(
patchNum: PatchSetNum,
- revisions: RevisionInfo[]
+ revisions: (RevisionInfo | EditRevisionInfo)[]
) {
revisions = revisions || [];
- const findNum = (rev: RevisionInfo) => `${rev._number}` === `${patchNum}`;
+ const findNum = (rev: RevisionInfo | EditRevisionInfo) =>
+ `${rev._number}` === `${patchNum}`;
return revisions.findIndex(findNum);
}
diff --git a/polygerrit-ui/app/utils/string-util.ts b/polygerrit-ui/app/utils/string-util.ts
index 43c0765..0b217ec 100644
--- a/polygerrit-ui/app/utils/string-util.ts
+++ b/polygerrit-ui/app/utils/string-util.ts
@@ -38,3 +38,12 @@
if (n % 10 === 3 && n % 100 !== 13) return `${n}rd`;
return `${n}th`;
}
+
+/**
+ * This converts any inputed value into string.
+ *
+ * This is so typescript checker doesn't fail.
+ */
+export function convertToString(key?: unknown) {
+ return key !== undefined ? String(key) : '';
+}
diff --git a/polygerrit-ui/package.json b/polygerrit-ui/package.json
index 793703e..25561bb 100644
--- a/polygerrit-ui/package.json
+++ b/polygerrit-ui/package.json
@@ -14,7 +14,7 @@
"@polymer/test-fixture": "^4.0.2",
"accessibility-developer-tools": "^2.12.0",
"chai": "^4.3.4",
- "karma": "^6.3.4",
+ "karma": "^6.3.6",
"karma-chrome-launcher": "^3.1.0",
"karma-mocha": "^2.0.1",
"karma-mocha-reporter": "^2.2.5",
diff --git a/polygerrit-ui/yarn.lock b/polygerrit-ui/yarn.lock
index 7c7ef45..e9c54e9 100644
--- a/polygerrit-ui/yarn.lock
+++ b/polygerrit-ui/yarn.lock
@@ -1094,7 +1094,7 @@
resolved "https://registry.yarnpkg.com/@types/content-disposition/-/content-disposition-0.5.4.tgz#de48cf01c79c9f1560bcfd8ae43217ab028657f8"
integrity sha512-0mPF08jn9zYI0n0Q/Pnz7C4kThdSt+6LD4amsrYDDpgBfrVWa3TcCOxKX1zkGgYniGagRv8heN2cbh+CAn+uuQ==
-"@types/cookie@^0.4.0":
+"@types/cookie@^0.4.1":
version "0.4.1"
resolved "https://registry.yarnpkg.com/@types/cookie/-/cookie-0.4.1.tgz#bfd02c1f2224567676c1545199f87c3a861d878d"
integrity sha512-XW/Aa8APYr6jSVVA1y/DEIZX0/GMKLEVekNG727R8cs56ahETkRAy/3DR7+fJyh7oUgGwNQaRfXCun0+KbWY7Q==
@@ -1109,7 +1109,7 @@
"@types/keygrip" "*"
"@types/node" "*"
-"@types/cors@^2.8.8":
+"@types/cors@^2.8.12":
version "2.8.12"
resolved "https://registry.yarnpkg.com/@types/cors/-/cors-2.8.12.tgz#6b2c510a7ad7039e98e7b8d3d6598f4359e5c080"
integrity sha512-vt+kDhq/M2ayberEtJcIN/hxXy1Pk+59g2FV/ZQceeaTyCtCucjL2Q7FXlFjtWn4n15KCr1NE2lNNFhp0lEThw==
@@ -1498,10 +1498,10 @@
resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee"
integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==
-base64-arraybuffer@0.1.4:
- version "0.1.4"
- resolved "https://registry.yarnpkg.com/base64-arraybuffer/-/base64-arraybuffer-0.1.4.tgz#9818c79e059b1355f97e0428a017c838e90ba812"
- integrity sha1-mBjHngWbE1X5fgQooBfIOOkLqBI=
+base64-arraybuffer@~1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/base64-arraybuffer/-/base64-arraybuffer-1.0.1.tgz#87bd13525626db4a9838e00a508c2b73efcf348c"
+ integrity sha512-vFIUq7FdLtjZMhATwDul5RZWv2jpXQ09Pd6jcVEOvIsqCWTRFD/ONHNfyOS8dA/Ippi5dsIgpyKWKZaAKZltbA==
base64id@2.0.0, base64id@~2.0.0:
version "2.0.0"
@@ -1939,7 +1939,7 @@
dependencies:
ms "^2.1.1"
-debug@^4.1.0, debug@^4.1.1, debug@~4.3.1:
+debug@^4.1.0, debug@^4.1.1, debug@~4.3.1, debug@~4.3.2:
version "4.3.2"
resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.2.tgz#f0a49c18ac8779e31d4a0c6029dfb76873c7428b"
integrity sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw==
@@ -2085,25 +2085,28 @@
dependencies:
once "^1.4.0"
-engine.io-parser@~4.0.0:
- version "4.0.2"
- resolved "https://registry.yarnpkg.com/engine.io-parser/-/engine.io-parser-4.0.2.tgz#e41d0b3fb66f7bf4a3671d2038a154024edb501e"
- integrity sha512-sHfEQv6nmtJrq6TKuIz5kyEKH/qSdK56H/A+7DnAuUPWosnIZAS2NHNcPLmyjtY3cGS/MqJdZbUjW97JU72iYg==
+engine.io-parser@~5.0.0:
+ version "5.0.1"
+ resolved "https://registry.yarnpkg.com/engine.io-parser/-/engine.io-parser-5.0.1.tgz#6695fc0f1e6d76ad4a48300ff80db5f6b3654939"
+ integrity sha512-j4p3WwJrG2k92VISM0op7wiq60vO92MlF3CRGxhKHy9ywG1/Dkc72g0dXeDQ+//hrcDn8gqQzoEkdO9FN0d9AA==
dependencies:
- base64-arraybuffer "0.1.4"
+ base64-arraybuffer "~1.0.1"
-engine.io@~4.1.0:
- version "4.1.1"
- resolved "https://registry.yarnpkg.com/engine.io/-/engine.io-4.1.1.tgz#9a8f8a5ac5a5ea316183c489bf7f5b6cf91ace5b"
- integrity sha512-t2E9wLlssQjGw0nluF6aYyfX8LwYU8Jj0xct+pAhfWfv/YrBn6TSNtEYsgxHIfaMqfrLx07czcMg9bMN6di+3w==
+engine.io@~6.0.0:
+ version "6.0.0"
+ resolved "https://registry.yarnpkg.com/engine.io/-/engine.io-6.0.0.tgz#2b993fcd73e6b3a6abb52b40b803651cd5747cf0"
+ integrity sha512-Ui7yl3JajEIaACg8MOUwWvuuwU7jepZqX3BKs1ho7NQRuP4LhN4XIykXhp8bEy+x/DhA0LBZZXYSCkZDqrwMMg==
dependencies:
+ "@types/cookie" "^0.4.1"
+ "@types/cors" "^2.8.12"
+ "@types/node" ">=10.0.0"
accepts "~1.3.4"
base64id "2.0.0"
cookie "~0.4.1"
cors "~2.8.5"
debug "~4.3.1"
- engine.io-parser "~4.0.0"
- ws "~7.4.2"
+ engine.io-parser "~5.0.0"
+ ws "~8.2.3"
ent@~2.2.0:
version "2.2.0"
@@ -2833,10 +2836,10 @@
dependencies:
minimist "^1.2.3"
-karma@^6.3.4:
- version "6.3.4"
- resolved "https://registry.yarnpkg.com/karma/-/karma-6.3.4.tgz#359899d3aab3d6b918ea0f57046fd2a6b68565e6"
- integrity sha512-hbhRogUYIulfkBTZT7xoPrCYhRBnBoqbbL4fszWD0ReFGUxU+LYBr3dwKdAluaDQ/ynT9/7C+Lf7pPNW4gSx4Q==
+karma@^6.3.6:
+ version "6.3.6"
+ resolved "https://registry.yarnpkg.com/karma/-/karma-6.3.6.tgz#6f64cdd558c7d0c9da6fcdece156089582694611"
+ integrity sha512-xsiu3D6AjCv6Uq0YKXJgC6TvXX2WloQ5+XtHXmC1lwiLVG617DDV3W2DdM4BxCMKHlmz6l3qESZHFQGHAKvrew==
dependencies:
body-parser "^1.19.0"
braces "^3.0.2"
@@ -2856,10 +2859,10 @@
qjobs "^1.2.0"
range-parser "^1.2.1"
rimraf "^3.0.2"
- socket.io "^3.1.0"
+ socket.io "^4.2.0"
source-map "^0.6.1"
tmp "^0.2.1"
- ua-parser-js "^0.7.28"
+ ua-parser-js "^0.7.30"
yargs "^16.1.1"
keygrip@~1.1.0:
@@ -3741,12 +3744,12 @@
nise "^5.0.1"
supports-color "^7.1.0"
-socket.io-adapter@~2.1.0:
- version "2.1.0"
- resolved "https://registry.yarnpkg.com/socket.io-adapter/-/socket.io-adapter-2.1.0.tgz#edc5dc36602f2985918d631c1399215e97a1b527"
- integrity sha512-+vDov/aTsLjViYTwS9fPy5pEtTkrbEKsw2M+oVSoFGw6OD1IpvlV1VPhUzNbofCQ8oyMbdYJqDtGdmHQK6TdPg==
+socket.io-adapter@~2.3.2:
+ version "2.3.2"
+ resolved "https://registry.yarnpkg.com/socket.io-adapter/-/socket.io-adapter-2.3.2.tgz#039cd7c71a52abad984a6d57da2c0b7ecdd3c289"
+ integrity sha512-PBZpxUPYjmoogY0aoaTmo1643JelsaS1CiAwNjRVdrI0X9Seuc19Y2Wife8k88avW6haG8cznvwbubAZwH4Mtg==
-socket.io-parser@~4.0.3:
+socket.io-parser@~4.0.4:
version "4.0.4"
resolved "https://registry.yarnpkg.com/socket.io-parser/-/socket.io-parser-4.0.4.tgz#9ea21b0d61508d18196ef04a2c6b9ab630f4c2b0"
integrity sha512-t+b0SS+IxG7Rxzda2EVvyBZbvFPBCjJoyHuE0P//7OAsN23GItzDRdWa6ALxZI/8R5ygK7jAR6t028/z+7295g==
@@ -3755,20 +3758,17 @@
component-emitter "~1.3.0"
debug "~4.3.1"
-socket.io@^3.1.0:
- version "3.1.2"
- resolved "https://registry.yarnpkg.com/socket.io/-/socket.io-3.1.2.tgz#06e27caa1c4fc9617547acfbb5da9bc1747da39a"
- integrity sha512-JubKZnTQ4Z8G4IZWtaAZSiRP3I/inpy8c/Bsx2jrwGrTbKeVU5xd6qkKMHpChYeM3dWZSO0QACiGK+obhBNwYw==
+socket.io@^4.2.0:
+ version "4.3.1"
+ resolved "https://registry.yarnpkg.com/socket.io/-/socket.io-4.3.1.tgz#c0aa14f3f916a8ab713e83a5bd20c16600245763"
+ integrity sha512-HC5w5Olv2XZ0XJ4gOLGzzHEuOCfj3G0SmoW3jLHYYh34EVsIr3EkW9h6kgfW+K3TFEcmYy8JcPWe//KUkBp5jA==
dependencies:
- "@types/cookie" "^0.4.0"
- "@types/cors" "^2.8.8"
- "@types/node" ">=10.0.0"
accepts "~1.3.4"
base64id "~2.0.0"
- debug "~4.3.1"
- engine.io "~4.1.0"
- socket.io-adapter "~2.1.0"
- socket.io-parser "~4.0.3"
+ debug "~4.3.2"
+ engine.io "~6.0.0"
+ socket.io-adapter "~2.3.2"
+ socket.io-parser "~4.0.4"
source-map-support@^0.5.19, source-map-support@~0.5.12:
version "0.5.19"
@@ -4056,10 +4056,10 @@
resolved "https://registry.yarnpkg.com/typical/-/typical-5.2.0.tgz#4daaac4f2b5315460804f0acf6cb69c52bb93066"
integrity sha512-dvdQgNDNJo+8B2uBQoqdb11eUCE1JQXhvjC/CZtgvZseVd5TYMXnq0+vuUemXbd/Se29cTaUuPX3YIc2xgbvIg==
-ua-parser-js@^0.7.28:
- version "0.7.28"
- resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.28.tgz#8ba04e653f35ce210239c64661685bf9121dec31"
- integrity sha512-6Gurc1n//gjp9eQNXjD9O3M/sMwVtN5S8Lv9bvOYBfKfDNiIIhqiyi01vMBO45u4zkDE420w/e0se7Vs+sIg+g==
+ua-parser-js@^0.7.30:
+ version "0.7.30"
+ resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.30.tgz#4cf5170e8b55ac553fe8b38df3a82f0669671f0b"
+ integrity sha512-uXEtSresNUlXQ1QL4/3dQORcGv7+J2ookOG2ybA/ga9+HYEXueT2o+8dUJQkpedsyTyCJ6jCCirRcKtdtx1kbg==
unicode-canonical-property-names-ecmascript@^1.0.4:
version "1.0.4"
@@ -4218,10 +4218,10 @@
resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f"
integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=
-ws@~7.4.2:
- version "7.4.6"
- resolved "https://registry.yarnpkg.com/ws/-/ws-7.4.6.tgz#5654ca8ecdeee47c33a9a4bf6d28e2be2980377c"
- integrity sha512-YmhHDO4MzaDLB+M9ym/mDA5z0naX8j7SIlT8f8z+I0VtzsRbekxEutHSme7NPS2qE8StCYQNUnfWdXta/Yu85A==
+ws@~8.2.3:
+ version "8.2.3"
+ resolved "https://registry.yarnpkg.com/ws/-/ws-8.2.3.tgz#63a56456db1b04367d0b721a0b80cae6d8becbba"
+ integrity sha512-wBuoj1BDpC6ZQ1B7DWQBYVLphPWkm8i9Y0/3YdHjHKHiohOJ1ws+3OccDWtH+PoC9DZD5WOTrJvNbWvjS6JWaA==
y18n@^5.0.5:
version "5.0.8"
diff --git a/resources/com/google/gerrit/server/mime/mime-types.properties b/resources/com/google/gerrit/server/mime/mime-types.properties
index fd2280c..01ca71c 100644
--- a/resources/com/google/gerrit/server/mime/mime-types.properties
+++ b/resources/com/google/gerrit/server/mime/mime-types.properties
@@ -67,6 +67,7 @@
f = text/x-fortran
factor = text/x-factor
feathre = text/x-feature
+feature = text/x-gherkin
fcl = text/x-fcl
for = text/x-fortran
formula = text/x-spreadsheet