Merge changes Iacfa133f,I8da4d776,I905b9968,Ie078f540
* changes:
Add REST API for migrating label functions to submit requirements
MigrateLabelFunctionsToSubmitRequirement: use ProjectConfig API instead of Config
MigrateLabelFunctionsToSubmitRequirement: use @Inject from Guava
MigrateLabelFunctionsToSubmitRequirement: remove redundant variable
diff --git a/Documentation/rest-api-projects.txt b/Documentation/rest-api-projects.txt
index f85f284..16dab80 100644
--- a/Documentation/rest-api-projects.txt
+++ b/Documentation/rest-api-projects.txt
@@ -989,7 +989,7 @@
----
[[create-config-change]]
-=== Create Config Change for review.
+=== Create Config Change for review
--
'PUT /projects/link:#project-name[\{project-name\}]/config:review'
--
@@ -1342,7 +1342,7 @@
----
[[create-change]]
-=== Create Change for review.
+=== Create Change for review
This endpoint is functionally equivalent to
link:rest-api-changes.html#create-change[create change in the change
@@ -1393,7 +1393,7 @@
----
[[create-access-change]]
-=== Create Access Rights Change for review.
+=== Create Access Rights Change for review
--
'PUT /projects/link:rest-api-projects.html#project-name[\{project-name\}]/access:review
--
@@ -3781,7 +3781,7 @@
----
[[create-labels-change]]
-=== Create Labels Change for review.
+=== Create Labels Change for review
--
'POST /projects/link:#project-name[\{project-name\}]/labels:review'
--
@@ -4095,7 +4095,7 @@
----
[[create-submit-requirements-change]]
-=== Create Submit Requirements Change for review.
+=== Create Submit Requirements Change for review
--
'POST /projects/link:#project-name[\{project-name\}]/submit_requirements:review'
--
diff --git a/java/com/google/gerrit/httpd/raw/IndexHtmlUtil.java b/java/com/google/gerrit/httpd/raw/IndexHtmlUtil.java
index 2ef499f..2ed32e7 100644
--- a/java/com/google/gerrit/httpd/raw/IndexHtmlUtil.java
+++ b/java/com/google/gerrit/httpd/raw/IndexHtmlUtil.java
@@ -19,6 +19,7 @@
import com.google.common.base.Strings;
import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSet;
import com.google.common.flogger.FluentLogger;
import com.google.common.primitives.Ints;
import com.google.gerrit.common.Nullable;
@@ -27,12 +28,14 @@
import com.google.gerrit.extensions.api.GerritApi;
import com.google.gerrit.extensions.api.accounts.AccountApi;
import com.google.gerrit.extensions.api.config.Server;
+import com.google.gerrit.extensions.client.ListChangesOption;
import com.google.gerrit.extensions.client.ListOption;
import com.google.gerrit.extensions.restapi.AuthException;
import com.google.gerrit.extensions.restapi.RestApiException;
import com.google.gerrit.httpd.raw.IndexPreloadingUtil.RequestedPage;
import com.google.gerrit.json.OutputFormat;
import com.google.gerrit.server.experiments.ExperimentFeatures;
+import com.google.gerrit.server.experiments.ExperimentFeaturesConstants;
import com.google.gson.Gson;
import com.google.template.soy.data.SanitizedContent;
import java.net.URI;
@@ -41,6 +44,7 @@
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
+import java.util.LinkedHashSet;
import java.util.Map;
import java.util.Set;
import java.util.function.Function;
@@ -68,10 +72,13 @@
String requestedURL)
throws URISyntaxException, RestApiException {
ImmutableMap.Builder<String, Object> data = ImmutableMap.builder();
+ boolean asyncSubmitRequirements =
+ experimentFeatures.isFeatureEnabled(ExperimentFeaturesConstants.ASYNC_SUBMIT_REQUIREMENTS);
data.putAll(
staticTemplateData(
canonicalURL, cdnPath, faviconPath, urlParameterMap, urlInScriptTagOrdainer))
- .putAll(dynamicTemplateData(gerritApi, requestedURL, canonicalURL));
+ .putAll(
+ dynamicTemplateData(gerritApi, requestedURL, canonicalURL, asyncSubmitRequirements));
Set<String> enabledExperiments = new HashSet<>();
enabledExperiments.addAll(experimentFeatures.getEnabledExperimentFeatures());
// Add all experiments enabled through url
@@ -107,7 +114,10 @@
/** Returns dynamic parameters of {@code index.html}. */
public static ImmutableMap<String, Object> dynamicTemplateData(
- GerritApi gerritApi, String requestedURL, String canonicalURL)
+ GerritApi gerritApi,
+ String requestedURL,
+ String canonicalURL,
+ boolean asyncSubmitRequirements)
throws RestApiException, URISyntaxException {
ImmutableMap.Builder<String, Object> data = ImmutableMap.builder();
Map<String, SanitizedContent> initialData = new HashMap<>();
@@ -127,15 +137,21 @@
Integer basePatchNum = computeBasePatchNum(requestedPath);
switch (page) {
case CHANGE, DIFF -> {
- if (basePatchNum.equals(0)) {
+ LinkedHashSet<ListChangesOption> changeDetailOptions =
+ new LinkedHashSet<>(
+ basePatchNum.equals(0)
+ ? IndexPreloadingUtil.CHANGE_DETAIL_OPTIONS_WITHOUT_PARENTS
+ : IndexPreloadingUtil.CHANGE_DETAIL_OPTIONS_WITH_PARENTS);
+ if (asyncSubmitRequirements) {
+ changeDetailOptions.remove(ListChangesOption.SUBMIT_REQUIREMENTS);
+ changeDetailOptions.remove(ListChangesOption.SUBMITTABLE);
data.put(
- "defaultChangeDetailHex",
- ListOption.toHex(IndexPreloadingUtil.CHANGE_DETAIL_OPTIONS_WITHOUT_PARENTS));
- } else {
- data.put(
- "defaultChangeDetailHex",
- ListOption.toHex(IndexPreloadingUtil.CHANGE_DETAIL_OPTIONS_WITH_PARENTS));
+ "submitRequirementsHex",
+ ListOption.toHex(
+ ImmutableSet.of(
+ ListChangesOption.SUBMIT_REQUIREMENTS, ListChangesOption.SUBMITTABLE)));
}
+ data.put("defaultChangeDetailHex", ListOption.toHex(changeDetailOptions));
data.put(
"changeRequestsPath",
IndexPreloadingUtil.computeChangeRequestsPath(requestedPath, page).get());
diff --git a/java/com/google/gerrit/server/cache/h2/H2CacheFactory.java b/java/com/google/gerrit/server/cache/h2/H2CacheFactory.java
index 039a33e..ab4c852 100644
--- a/java/com/google/gerrit/server/cache/h2/H2CacheFactory.java
+++ b/java/com/google/gerrit/server/cache/h2/H2CacheFactory.java
@@ -146,19 +146,20 @@
cleanup.shutdownNow();
}
- List<Runnable> pending = executor.shutdownNow();
- if (executor.awaitTermination(15, TimeUnit.MINUTES)) {
- if (pending != null && !pending.isEmpty()) {
- logger.atInfo().log("Finishing %d disk cache updates", pending.size());
- for (Runnable update : pending) {
- update.run();
- }
+ executor.shutdown();
+ if (!executor.awaitTermination(15, TimeUnit.MINUTES)) {
+ logger.atInfo().log(
+ "Timeout elapsed waiting for storage tasks to terminate, shutting down now.");
+ List<Runnable> pending = executor.shutdownNow();
+ logger.atInfo().log("Canceling %d pending storage tasks.", pending.size());
+ if (!executor.awaitTermination(1, TimeUnit.MINUTES)) {
+ logger.atInfo().log("Timeout elapsed waiting for cancelled tasks to terminate.");
}
- } else {
- logger.atInfo().log("Timeout waiting for disk cache to close");
}
} catch (InterruptedException e) {
logger.atWarning().log("Interrupted waiting for disk cache to shutdown");
+ executor.shutdownNow();
+ Thread.currentThread().interrupt();
}
}
synchronized (caches) {
@@ -223,6 +224,7 @@
if (h2AutoServer) {
url.append(";AUTO_SERVER=TRUE");
}
+ url.append(";DB_CLOSE_DELAY=-1");
Duration refreshAfterWrite = def.refreshAfterWrite();
if (has(def.configKey(), "refreshAfterWrite")) {
long refreshAfterWriteInSec =
diff --git a/java/com/google/gerrit/server/cache/h2/H2CacheImpl.java b/java/com/google/gerrit/server/cache/h2/H2CacheImpl.java
index eb557b8..39afb38 100644
--- a/java/com/google/gerrit/server/cache/h2/H2CacheImpl.java
+++ b/java/com/google/gerrit/server/cache/h2/H2CacheImpl.java
@@ -161,7 +161,7 @@
() -> {
ValueHolder<V> h = store.getIfPresent(key);
if (h == null) {
- h = new ValueHolder<>(valueLoader.call(), Instant.ofEpochMilli(TimeUtil.nowMs()));
+ h = new ValueHolder<>(valueLoader.call(), TimeUtil.now());
ValueHolder<V> fh = h;
executor.execute(() -> store.put(key, fh));
}
@@ -172,7 +172,7 @@
@Override
public void put(K key, V val) {
- final ValueHolder<V> h = new ValueHolder<>(val, Instant.ofEpochMilli(TimeUtil.nowMs()));
+ final ValueHolder<V> h = new ValueHolder<>(val, TimeUtil.now());
mem.put(key, h);
executor.execute(() -> store.put(key, h));
}
@@ -260,7 +260,7 @@
"Loading value from cache", Metadata.builder().cacheKey(key.toString()).build())) {
ValueHolder<V> h = store.getIfPresent(key);
if (h == null) {
- h = new ValueHolder<>(loader.load(key), Instant.ofEpochMilli(TimeUtil.nowMs()));
+ h = new ValueHolder<>(loader.load(key), TimeUtil.now());
ValueHolder<V> fh = h;
executor.execute(() -> store.put(key, fh));
}
@@ -283,7 +283,7 @@
}
try {
Map<K, V> remaining = loader.loadAll(notInMemory);
- Instant instant = Instant.ofEpochMilli(TimeUtil.nowMs());
+ Instant instant = TimeUtil.now();
storeInDatabase(remaining, instant);
remaining
.entrySet()
@@ -345,6 +345,7 @@
private final boolean buildBloomFilter;
private boolean trackLastAccess;
private final AtomicBoolean isDiskCacheReadOnly;
+ private volatile boolean ensuredSchemaCreation;
SqlStore(
String jdbcUrl,
@@ -389,6 +390,38 @@
return new ObjectKeyTypeImpl<>(serializer);
}
+ private void createSchema() throws SQLException {
+ if (!ensuredSchemaCreation) {
+ synchronized (this) {
+ if (ensuredSchemaCreation) {
+ return;
+ }
+ try (SqlHandle h = new SqlHandle(url)) {
+ try (Statement stmt = h.conn.createStatement()) {
+ stmt.addBatch(
+ "CREATE TABLE IF NOT EXISTS data"
+ + "(k "
+ + keyType.columnType()
+ + " NOT NULL PRIMARY KEY HASH"
+ + ",v OTHER NOT NULL"
+ + ",created TIMESTAMP NOT NULL"
+ + ",accessed TIMESTAMP NOT NULL"
+ + ")");
+ stmt.addBatch(
+ "ALTER TABLE data ADD COLUMN IF NOT EXISTS "
+ + "space BIGINT AS OCTET_LENGTH(k) + OCTET_LENGTH(v)");
+ stmt.addBatch(
+ "ALTER TABLE data ADD COLUMN IF NOT EXISTS version INT DEFAULT 0 NOT NULL");
+ stmt.addBatch("CREATE INDEX IF NOT EXISTS version_key ON data(version, k)");
+ stmt.addBatch("CREATE INDEX IF NOT EXISTS accessed ON data(accessed)");
+ stmt.executeBatch();
+ ensuredSchemaCreation = true;
+ }
+ }
+ }
+ }
+ }
+
void open() {
bloomFilter.initIfNeeded();
}
@@ -398,6 +431,17 @@
while ((h = handles.poll()) != null) {
h.close();
}
+ shutdown();
+ }
+
+ private void shutdown() {
+ try (SqlHandle h = new SqlHandle(url)) {
+ try (Statement stmt = h.conn.createStatement()) {
+ stmt.execute("SHUTDOWN");
+ }
+ } catch (SQLException e) {
+ logger.atSevere().withCause(e).log("Cannot shutdown cache %s", url);
+ }
}
boolean mightContain(K key) {
@@ -728,8 +772,9 @@
}
private SqlHandle acquire() throws SQLException {
+ createSchema();
SqlHandle h = handles.poll();
- return h != null ? h : new SqlHandle(url, keyType);
+ return h != null ? h : new SqlHandle(url);
}
private void release(SqlHandle h) {
@@ -747,7 +792,7 @@
}
}
- static class SqlHandle {
+ static class SqlHandle implements AutoCloseable {
private final String url;
Connection conn;
PreparedStatement get;
@@ -755,30 +800,13 @@
PreparedStatement touch;
PreparedStatement invalidate;
- SqlHandle(String url, KeyType<?> type) throws SQLException {
+ SqlHandle(String url) throws SQLException {
this.url = url;
this.conn = org.h2.Driver.load().connect(url, null);
- try (Statement stmt = conn.createStatement()) {
- stmt.addBatch(
- "CREATE TABLE IF NOT EXISTS data"
- + "(k "
- + type.columnType()
- + " NOT NULL PRIMARY KEY HASH"
- + ",v OTHER NOT NULL"
- + ",created TIMESTAMP NOT NULL"
- + ",accessed TIMESTAMP NOT NULL"
- + ")");
- stmt.addBatch(
- "ALTER TABLE data ADD COLUMN IF NOT EXISTS "
- + "space BIGINT AS OCTET_LENGTH(k) + OCTET_LENGTH(v)");
- stmt.addBatch("ALTER TABLE data ADD COLUMN IF NOT EXISTS version INT DEFAULT 0 NOT NULL");
- stmt.addBatch("CREATE INDEX IF NOT EXISTS version_key ON data(version, k)");
- stmt.addBatch("CREATE INDEX IF NOT EXISTS accessed ON data(accessed)");
- stmt.executeBatch();
- }
}
- void close() {
+ @Override
+ public void close() {
get = closeStatement(get);
put = closeStatement(put);
touch = closeStatement(touch);
diff --git a/java/com/google/gerrit/server/experiments/ExperimentFeaturesConstants.java b/java/com/google/gerrit/server/experiments/ExperimentFeaturesConstants.java
index cd91745..ce96132 100644
--- a/java/com/google/gerrit/server/experiments/ExperimentFeaturesConstants.java
+++ b/java/com/google/gerrit/server/experiments/ExperimentFeaturesConstants.java
@@ -60,4 +60,7 @@
/** Whether we allow fix suggestions in HumanComments. */
public static final String ALLOW_FIX_SUGGESTIONS_IN_COMMENTS =
"GerritBackendFeature__allow_fix_suggestions_in_comments";
+
+ /** Whether UI should request Submit Requirements separately from change detail. */
+ public static final String ASYNC_SUBMIT_REQUIREMENTS = "UiFeature__async_submit_requirements";
}
diff --git a/java/com/google/gerrit/server/git/CommitUtil.java b/java/com/google/gerrit/server/git/CommitUtil.java
index ff729b7..291d5a9 100644
--- a/java/com/google/gerrit/server/git/CommitUtil.java
+++ b/java/com/google/gerrit/server/git/CommitUtil.java
@@ -321,7 +321,16 @@
}
if (footersToAdd.length() > 0) {
- message = message.trim() + "\n\n" + footersToAdd.toString().trim();
+ String trimmedMsg = message.trim();
+ List<String> lines = Splitter.on('\n').splitToList(trimmedMsg);
+ String lastLine = lines.isEmpty() ? "" : lines.get(lines.size() - 1);
+ boolean endsWithFooter = lastLine.matches("^[a-zA-Z0-9-]+:.*");
+
+ if (endsWithFooter) {
+ message = trimmedMsg + "\n" + footersToAdd.toString().trim();
+ } else {
+ message = trimmedMsg + "\n\n" + footersToAdd.toString().trim();
+ }
}
}
diff --git a/java/com/google/gerrit/server/restapi/flow/IsFlowsEnabled.java b/java/com/google/gerrit/server/restapi/flow/IsFlowsEnabled.java
index 941e87e..a4e2c24 100644
--- a/java/com/google/gerrit/server/restapi/flow/IsFlowsEnabled.java
+++ b/java/com/google/gerrit/server/restapi/flow/IsFlowsEnabled.java
@@ -19,9 +19,11 @@
import com.google.gerrit.extensions.restapi.RestApiException;
import com.google.gerrit.extensions.restapi.RestReadView;
import com.google.gerrit.server.change.ChangeResource;
+import com.google.gerrit.server.flow.FlowService;
import com.google.gerrit.server.flow.FlowServiceUtil;
import com.google.inject.Inject;
import com.google.inject.Singleton;
+import java.util.Optional;
/**
* REST endpoint to check if Flows is enabled for the user.
@@ -39,11 +41,13 @@
@Override
public Response<IsFlowsEnabledInfo> apply(ChangeResource changeResource) throws RestApiException {
+ Optional<FlowService> flowService = flowServiceUtil.getFlowService();
+ if (flowService.isEmpty()) {
+ return Response.ok(new IsFlowsEnabledInfo(false));
+ }
IsFlowsEnabledInfo enabledInfo =
new IsFlowsEnabledInfo(
- flowServiceUtil
- .getFlowServiceOrThrow()
- .isFlowsEnabled(changeResource.getProject(), changeResource.getId()));
+ flowService.get().isFlowsEnabled(changeResource.getProject(), changeResource.getId()));
return Response.ok(enabledInfo);
}
}
diff --git a/javatests/com/google/gerrit/acceptance/api/change/RevertIT.java b/javatests/com/google/gerrit/acceptance/api/change/RevertIT.java
index a8f3db4..b3b56db 100644
--- a/javatests/com/google/gerrit/acceptance/api/change/RevertIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/change/RevertIT.java
@@ -1757,10 +1757,54 @@
// Check that the footers are not duplicated
String commitMessage = gApi.changes().id(revertChange.id).current().commit(false).message;
- int bugOccurrences = commitMessage.split("Bug: 12345", -1).length - 1;
- assertThat(bugOccurrences).isEqualTo(1);
- int issueOccurrences = commitMessage.split("Issue: 67890", -1).length - 1;
- assertThat(issueOccurrences).isEqualTo(1);
+ String expectedMessage =
+ String.format(
+ """
+ Reverting this change
+
+ Bug: 12345
+ Issue: 67890
+ Change-Id: %s
+ """,
+ revertChange.changeId);
+ assertThat(commitMessage).isEqualTo(expectedMessage);
+ }
+
+ @Test
+ public void revertChangeWithSkipPresubmitFooter() throws Exception {
+ // Create a change with bug and issue footers
+ PushOneCommit push =
+ pushFactory.create(
+ admin.newIdent(),
+ testRepo,
+ "Change with bug and issue\n" + "\n" + "Bug: 12345\n" + "Issue: 67890",
+ "a.txt",
+ "content");
+ PushOneCommit.Result r = push.to("refs/for/master");
+ r.assertOkStatus();
+
+ gApi.changes().id(r.getChangeId()).revision(r.getCommit().name()).review(ReviewInput.approve());
+ gApi.changes().id(r.getChangeId()).revision(r.getCommit().name()).submit();
+
+ // Revert the change
+ RevertInput revertInput = new RevertInput();
+ revertInput.message = "Reverting this change\n\nSkip-Presubmit: true";
+ ChangeInfo revertChange = gApi.changes().id(r.getChangeId()).revert(revertInput).get();
+
+ // Check that the revert commit message contains all footers and no extra newline
+ String commitMessage = gApi.changes().id(revertChange.id).current().commit(false).message;
+ String expectedMessage =
+ String.format(
+ """
+ Reverting this change
+
+ Skip-Presubmit: true
+ Bug: 12345
+ Issue: 67890
+ Change-Id: %s
+ """,
+ revertChange.changeId);
+ assertThat(commitMessage).isEqualTo(expectedMessage);
}
@Test
@@ -1780,8 +1824,6 @@
gApi.changes().id(r.getChangeId()).revision(r.getCommit().name()).submit();
// Revert the submission with a message that already contains the footers
- RevertInput revertInput = new RevertInput();
- revertInput.message = "Reverting this change\n\nBug: 12345\nIssue: 67890";
List<ChangeInfo> revertChanges =
gApi.changes().id(r.getChangeId()).revertSubmission().revertChanges;
assertThat(revertChanges).hasSize(1);
@@ -1789,10 +1831,19 @@
// Check that the footers are not duplicated
String commitMessage = gApi.changes().id(revertChange.id).current().commit(false).message;
- int bugOccurrences = commitMessage.split("Bug: 12345", -1).length - 1;
- assertThat(bugOccurrences).isEqualTo(1);
- int issueOccurrences = commitMessage.split("Issue: 67890", -1).length - 1;
- assertThat(issueOccurrences).isEqualTo(1);
+ String expectedMessage =
+ String.format(
+ """
+ Revert "Change with bug and issue"
+
+ This reverts commit %s.
+
+ Bug: 12345
+ Issue: 67890
+ Change-Id: %s
+ """,
+ r.getCommit().name(), revertChange.changeId);
+ assertThat(commitMessage).isEqualTo(expectedMessage);
}
@Override
diff --git a/javatests/com/google/gerrit/acceptance/api/config/ListExperimentsIT.java b/javatests/com/google/gerrit/acceptance/api/config/ListExperimentsIT.java
index fe3cb00..8dcd0f3 100644
--- a/javatests/com/google/gerrit/acceptance/api/config/ListExperimentsIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/config/ListExperimentsIT.java
@@ -52,7 +52,8 @@
.GERRIT_BACKEND_FEATURE_ALWAYS_REJECT_IMPLICIT_MERGES_ON_MERGE,
ExperimentFeaturesConstants.GERRIT_BACKEND_FEATURE_ATTACH_NONCE_TO_DOCUMENTATION,
ExperimentFeaturesConstants.GERRIT_BACKEND_FEATURE_CHECK_IMPLICIT_MERGES_ON_MERGE,
- ExperimentFeaturesConstants.GERRIT_BACKEND_FEATURE_REJECT_IMPLICIT_MERGES_ON_MERGE)
+ ExperimentFeaturesConstants.GERRIT_BACKEND_FEATURE_REJECT_IMPLICIT_MERGES_ON_MERGE,
+ ExperimentFeaturesConstants.ASYNC_SUBMIT_REQUIREMENTS)
.inOrder();
// "GerritBackendFeature__check_implicit_merges_on_merge",
diff --git a/javatests/com/google/gerrit/httpd/raw/IndexHtmlUtilTest.java b/javatests/com/google/gerrit/httpd/raw/IndexHtmlUtilTest.java
index 2f6d565..68be60b 100644
--- a/javatests/com/google/gerrit/httpd/raw/IndexHtmlUtilTest.java
+++ b/javatests/com/google/gerrit/httpd/raw/IndexHtmlUtilTest.java
@@ -117,7 +117,7 @@
String requestedPath = "/c/project/+/123/4..6";
assertThat(IndexHtmlUtil.computeBasePatchNum(requestedPath)).isEqualTo(4);
- assertThat(dynamicTemplateData(gerritApi, requestedPath, ""))
+ assertThat(dynamicTemplateData(gerritApi, requestedPath, "", false))
.containsAtLeast(
"defaultChangeDetailHex", "9996394",
"changeRequestsPath", "changes/project~123");
@@ -145,12 +145,41 @@
String requestedPath = "/c/project/+/123";
assertThat(IndexHtmlUtil.computeBasePatchNum(requestedPath)).isEqualTo(0);
- assertThat(dynamicTemplateData(gerritApi, requestedPath, ""))
+ assertThat(dynamicTemplateData(gerritApi, requestedPath, "", false))
.containsAtLeast(
"defaultChangeDetailHex", "1996394",
"changeRequestsPath", "changes/project~123");
}
+ @Test
+ public void usePreloadRestWithAsyncSRs() throws Exception {
+ Accounts accountsApi = mock(Accounts.class);
+ when(accountsApi.self()).thenThrow(new AuthException("user needs to be authenticated"));
+
+ Server serverApi = mock(Server.class);
+ when(serverApi.getVersion()).thenReturn("123");
+ when(serverApi.topMenus()).thenReturn(ImmutableList.of());
+ ServerInfo serverInfo = new ServerInfo();
+ serverInfo.defaultTheme = "my-default-theme";
+ when(serverApi.getInfo()).thenReturn(serverInfo);
+
+ Config configApi = mock(Config.class);
+ when(configApi.server()).thenReturn(serverApi);
+
+ GerritApi gerritApi = mock(GerritApi.class);
+ when(gerritApi.accounts()).thenReturn(accountsApi);
+ when(gerritApi.config()).thenReturn(configApi);
+
+ String requestedPath = "/c/project/+/123";
+ assertThat(IndexHtmlUtil.computeBasePatchNum(requestedPath)).isEqualTo(0);
+
+ assertThat(dynamicTemplateData(gerritApi, requestedPath, "", true))
+ .containsAtLeast(
+ "defaultChangeDetailHex", "896394",
+ "submitRequirementsHex", "1100000",
+ "changeRequestsPath", "changes/project~123");
+ }
+
private static SanitizedContent ordain(String s) {
return UnsafeSanitizedContentOrdainer.ordainAsSafe(
s, SanitizedContent.ContentKind.TRUSTED_RESOURCE_URI);
diff --git a/javatests/com/google/gerrit/server/cache/h2/H2CacheTest.java b/javatests/com/google/gerrit/server/cache/h2/H2CacheTest.java
index 8b6ba2a..cf8bdda 100644
--- a/javatests/com/google/gerrit/server/cache/h2/H2CacheTest.java
+++ b/javatests/com/google/gerrit/server/cache/h2/H2CacheTest.java
@@ -64,7 +64,7 @@
@Nullable Duration expireAfterWrite,
@Nullable Duration refreshAfterWrite) {
return new SqlStore<>(
- "jdbc:h2:mem:Test_" + id,
+ "jdbc:h2:mem:Test_" + id + ";DB_CLOSE_DELAY=-1",
KEY_TYPE,
StringCacheSerializer.INSTANCE,
StringCacheSerializer.INSTANCE,
diff --git a/polygerrit-ui/README.md b/polygerrit-ui/README.md
index ac51959..fc7bfba 100644
--- a/polygerrit-ui/README.md
+++ b/polygerrit-ui/README.md
@@ -233,6 +233,12 @@
yarn eslint
```
+* To run ESLint and apply changes on the whole app:
+
+```sh
+yarn eslintfix
+```
+
* To run ESLint on just the subdirectory you modified:
```sh
diff --git a/polygerrit-ui/app/api/plugin.ts b/polygerrit-ui/app/api/plugin.ts
index ed85fe2..7695b60 100644
--- a/polygerrit-ui/app/api/plugin.ts
+++ b/polygerrit-ui/app/api/plugin.ts
@@ -33,6 +33,7 @@
// Fires GerritView values such as 'change', 'dashboard', 'admin', ...
VIEW_CHANGE = 'view-change',
SHOW_REVISION_ACTIONS = 'show-revision-actions',
+ BEFORE_COMMIT_MSG_EDIT = 'before-commit-msg-edit',
COMMIT_MSG_EDIT = 'commitmsgedit',
CUSTOM_EMOJIS = 'custom-emojis',
REVERT = 'revert',
diff --git a/polygerrit-ui/app/api/rest-api.ts b/polygerrit-ui/app/api/rest-api.ts
index cb09c45..1e31feb 100644
--- a/polygerrit-ui/app/api/rest-api.ts
+++ b/polygerrit-ui/app/api/rest-api.ts
@@ -402,6 +402,9 @@
labels?: LabelNameToInfoMap;
permitted_labels?: LabelNameToValuesMap;
removable_reviewers?: AccountInfo[];
+ removable_labels?: {
+ [labelName: string]: {[labelValue: string]: AccountInfo[]};
+ };
// This is documented as optional, but actually always set.
reviewers: Reviewers;
pending_reviewers?: AccountInfo[];
@@ -1523,3 +1526,11 @@
delete?: string[];
commit_message?: string;
}
+
+/**
+ * The IsFlowsEnabledInfo entity contains information about whether flows are
+ * enabled.
+ */
+export declare interface IsFlowsEnabledInfo {
+ enabled: boolean;
+}
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item.ts b/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item.ts
index 29fdb92..9b06622 100644
--- a/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item.ts
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item.ts
@@ -360,7 +360,7 @@
<label class="selectionLabel">
<md-checkbox
?checked=${this.checked}
- @click=${this.toggleCheckbox}
+ @change=${this.toggleCheckbox}
></md-checkbox>
</label>
</td>
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list-section/gr-change-list-section.ts b/polygerrit-ui/app/elements/change-list/gr-change-list-section/gr-change-list-section.ts
index bd53670..d8c1a5d 100644
--- a/polygerrit-ui/app/elements/change-list/gr-change-list-section/gr-change-list-section.ts
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list-section/gr-change-list-section.ts
@@ -296,7 +296,7 @@
class="selection-checkbox"
?checked=${checked}
.indeterminate=${indeterminate}
- @click=${this.handleSelectAllCheckboxClicked}
+ @change=${this.handleSelectAllCheckboxClicked}
></md-checkbox>
</td>
`;
diff --git a/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.ts b/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.ts
index 216559c..0393242 100644
--- a/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.ts
+++ b/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.ts
@@ -54,6 +54,7 @@
RequestPayload,
RevertSubmissionInfo,
ReviewInput,
+ RevisionInfo,
} from '../../../types/common';
import {GrConfirmAbandonDialog} from '../gr-confirm-abandon-dialog/gr-confirm-abandon-dialog';
import {GrDialog} from '../../shared/gr-dialog/gr-dialog';
@@ -1361,7 +1362,10 @@
}
// private but used in test
- getRevision(change: ChangeInfo, patchNum?: PatchSetNumber) {
+ getRevision(
+ change: ChangeInfo,
+ patchNum?: PatchSetNumber
+ ): RevisionInfo | null {
for (const rev of Object.values(change.revisions ?? {})) {
if (rev._number === patchNum) {
return rev;
diff --git a/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions_test.ts b/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions_test.ts
index 7da1513..ed4c9c5 100644
--- a/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions_test.ts
+++ b/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions_test.ts
@@ -1676,7 +1676,7 @@
assert.equal(confirmRevertDialog.message, expectedMsg);
const radioInputs = queryAll<HTMLInputElement>(
confirmRevertDialog,
- 'input[name="revertOptions"]'
+ 'md-radio[name="revertOptions"]'
);
radioInputs[0].click();
await element.updateComplete;
@@ -1728,7 +1728,7 @@
await element.updateComplete;
const radioInputs = queryAll<HTMLInputElement>(
confirmRevertDialog,
- 'input[name="revertOptions"]'
+ 'md-radio[name="revertOptions"]'
);
const revertSubmissionMsg =
'Revert submission 199 0' +
diff --git a/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata_screenshot_test.ts b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata_screenshot_test.ts
index f45f5ae..87b50a2 100644
--- a/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata_screenshot_test.ts
+++ b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata_screenshot_test.ts
@@ -60,10 +60,11 @@
await element.updateComplete;
});
- test('normal view', async () => {
- await visualDiff(element, 'gr-change-metadata');
- await visualDiffDarkTheme(element, 'gr-change-metadata-dark');
- });
+ // TODO(b/447590232): Fix test flakiness and re-enable
+ // test('normal view', async () => {
+ // await visualDiff(element, 'gr-change-metadata');
+ // await visualDiffDarkTheme(element, 'gr-change-metadata-dark');
+ // });
test('show all sections with more data', async () => {
const changeModel = testResolver(changeModelToken);
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 017adca..591845a 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
@@ -4,6 +4,7 @@
* SPDX-License-Identifier: Apache-2.0
*/
import './gr-checks-chip';
+import './gr-summary-chip';
import '../gr-comments-summary/gr-comments-summary';
import '../../shared/gr-icon/gr-icon';
import '../../checks/gr-checks-action';
@@ -29,6 +30,7 @@
} from '../../../models/checks/checks-util';
import {getMentionedThreads, isUnresolved} from '../../../utils/comment-util';
import {AccountInfo, CommentThread, DropdownLink} from '../../../types/common';
+import {FlowInfo, FlowStageState} from '../../../api/rest-api';
import {Tab} from '../../../constants/constants';
import {ChecksTabState} from '../../../types/events';
import {modifierPressed} from '../../../utils/dom-util';
@@ -43,6 +45,7 @@
import {userModelToken} from '../../../models/user/user-model';
import {assertIsDefined} from '../../../utils/common-util';
import {GrAiPromptDialog} from '../gr-ai-prompt-dialog/gr-ai-prompt-dialog';
+import {flowsModelToken} from '../../../models/flows/flows-model';
function handleSpaceOrEnter(e: KeyboardEvent, handler: () => void) {
if (modifierPressed(e)) return;
@@ -98,6 +101,9 @@
@state()
draftCount = 0;
+ @state()
+ flows: FlowInfo[] = [];
+
@query('#aiPromptModal')
aiPromptModal?: HTMLDialogElement;
@@ -114,6 +120,8 @@
private readonly getChangeModel = resolve(this, changeModelToken);
+ private readonly getFlowsModel = resolve(this, flowsModelToken);
+
private readonly reporting = getAppContext().reportingService;
constructor() {
@@ -189,6 +197,11 @@
this.mentionCount = unresolvedThreadsMentioningSelf.length;
}
);
+ subscribe(
+ this,
+ () => this.getFlowsModel().flows$,
+ x => (this.flows = x)
+ );
}
static override get styles() {
@@ -584,7 +597,7 @@
</div>
</td>
</tr>
- ${this.renderChecksSummary()}
+ ${this.renderChecksSummary()} ${this.renderFlowsSummary()}
</table>
</div>
<dialog id="aiPromptModal" tabindex="-1">
@@ -596,6 +609,81 @@
`;
}
+ private getFlowOverallStatus(flow: FlowInfo): FlowStageState {
+ if (
+ flow.stages.some(
+ stage =>
+ stage.state === FlowStageState.FAILED ||
+ stage.state === FlowStageState.TERMINATED
+ )
+ ) {
+ return FlowStageState.FAILED;
+ }
+ if (
+ flow.stages.some(
+ stage =>
+ stage.state === FlowStageState.PENDING ||
+ stage.state === FlowStageState.TERMINATED
+ )
+ ) {
+ return FlowStageState.PENDING; // Using PENDING to represent running/in-progress
+ }
+ if (flow.stages.every(stage => stage.state === FlowStageState.DONE)) {
+ return FlowStageState.DONE;
+ }
+ return FlowStageState.PENDING; // Default or unknown state
+ }
+
+ private renderFlowsSummary() {
+ if (this.flows.length === 0) return nothing;
+ const handler = () => fireShowTab(this, Tab.FLOWS, true);
+ const failed = this.flows.filter(
+ f => this.getFlowOverallStatus(f) === FlowStageState.FAILED
+ ).length;
+ const running = this.flows.filter(
+ f => this.getFlowOverallStatus(f) === FlowStageState.PENDING
+ ).length;
+ const done = this.flows.filter(
+ f => this.getFlowOverallStatus(f) === FlowStageState.DONE
+ ).length;
+ return html`
+ <tr>
+ <td class="key">Flows</td>
+ <td class="value">
+ <div class="flowsSummary">
+ ${failed > 0
+ ? html`<gr-checks-chip
+ .statusOrCategory=${Category.ERROR}
+ .text=${`${failed}`}
+ @click=${handler}
+ @keydown=${(e: KeyboardEvent) =>
+ handleSpaceOrEnter(e, handler)}
+ ></gr-checks-chip>`
+ : ''}
+ ${running > 0
+ ? html`<gr-checks-chip
+ .statusOrCategory=${RunStatus.RUNNING}
+ .text=${`${running}`}
+ @click=${handler}
+ @keydown=${(e: KeyboardEvent) =>
+ handleSpaceOrEnter(e, handler)}
+ ></gr-checks-chip>`
+ : ''}
+ ${done > 0
+ ? html`<gr-checks-chip
+ .statusOrCategory=${Category.SUCCESS}
+ .text=${`${done}`}
+ @click=${handler}
+ @keydown=${(e: KeyboardEvent) =>
+ handleSpaceOrEnter(e, handler)}
+ ></gr-checks-chip>`
+ : ''}
+ </div>
+ </td>
+ </tr>
+ `;
+ }
+
private renderChecksSummary() {
const hasNonRunningChip = this.runs.some(
run => hasCompletedWithoutResults(run) || hasResults(run)
diff --git a/polygerrit-ui/app/elements/change/gr-change-summary/gr-change-summary_test.ts b/polygerrit-ui/app/elements/change/gr-change-summary/gr-change-summary_test.ts
index 2770ada..fd60d95 100644
--- a/polygerrit-ui/app/elements/change/gr-change-summary/gr-change-summary_test.ts
+++ b/polygerrit-ui/app/elements/change/gr-change-summary/gr-change-summary_test.ts
@@ -16,7 +16,7 @@
createDraft,
createRun,
} from '../../../test/test-data-generators';
-import {Timestamp} from '../../../api/rest-api';
+import {FlowInfo, FlowStageState, Timestamp} from '../../../api/rest-api';
import {testResolver} from '../../../test/common-test-setup';
import {UserModel, userModelToken} from '../../../models/user/user-model';
import {
@@ -26,16 +26,29 @@
import {GrChecksChip} from './gr-checks-chip';
import {CheckRun} from '../../../models/checks/checks-model';
import {Category, RunStatus} from '../../../api/checks';
+import {FlowsModel, flowsModelToken} from '../../../models/flows/flows-model';
+
+function createFlow(partial: Partial<FlowInfo> = {}): FlowInfo {
+ return {
+ uuid: 'test-uuid',
+ owner: createAccountWithEmail(),
+ created: '2020-01-01 00:00:00.000000000' as Timestamp,
+ stages: [],
+ ...partial,
+ };
+}
suite('gr-change-summary test', () => {
let element: GrChangeSummary;
let commentsModel: CommentsModel;
let userModel: UserModel;
+ let flowsModel: FlowsModel;
setup(async () => {
element = await fixture(html`<gr-change-summary></gr-change-summary>`);
commentsModel = testResolver(commentsModelToken);
userModel = testResolver(userModelToken);
+ flowsModel = testResolver(flowsModelToken);
});
test('is defined', () => {
@@ -58,7 +71,8 @@
await element.updateComplete;
assert.shadowDom.equal(
element,
- /* HTML */ `<div>
+ /* HTML */ `
+ <div>
<table class="info">
<tbody>
<tr>
@@ -86,7 +100,8 @@
<dialog id="aiPromptModal" tabindex="-1">
<gr-ai-prompt-dialog id="aiPromptDialog" role="dialog">
</gr-ai-prompt-dialog>
- </dialog> `
+ </dialog>
+ `
);
});
@@ -181,6 +196,62 @@
});
});
+ suite('flows summary', () => {
+ test('renders', async () => {
+ flowsModel.setState({
+ flows: [
+ createFlow({
+ stages: [
+ {expression: {condition: ''}, state: FlowStageState.PENDING},
+ ],
+ }),
+ createFlow({
+ stages: [{expression: {condition: ''}, state: FlowStageState.DONE}],
+ }),
+ createFlow({
+ stages: [{expression: {condition: ''}, state: FlowStageState.DONE}],
+ }),
+ createFlow({
+ stages: [
+ {expression: {condition: ''}, state: FlowStageState.FAILED},
+ ],
+ }),
+ createFlow({
+ stages: [
+ {expression: {condition: ''}, state: FlowStageState.FAILED},
+ ],
+ }),
+ createFlow({
+ stages: [
+ {expression: {condition: ''}, state: FlowStageState.FAILED},
+ ],
+ }),
+ ],
+ loading: false,
+ });
+ await element.updateComplete;
+ const flowsSummary = queryAndAssert(element, '.flowsSummary');
+ assert.dom.equal(
+ flowsSummary,
+ /* HTML */ `
+ <div class="flowsSummary">
+ <gr-checks-chip> </gr-checks-chip>
+ <gr-checks-chip> </gr-checks-chip>
+ <gr-checks-chip> </gr-checks-chip>
+ </div>
+ `
+ );
+ const chips = queryAll<GrChecksChip>(element, 'gr-checks-chip');
+ assert.equal(chips.length, 3);
+ assert.equal(chips[0].statusOrCategory, Category.ERROR);
+ assert.equal(chips[0].text, '3');
+ assert.equal(chips[1].statusOrCategory, RunStatus.RUNNING);
+ assert.equal(chips[1].text, '1');
+ assert.equal(chips[2].statusOrCategory, Category.SUCCESS);
+ assert.equal(chips[2].text, '2');
+ });
+ });
+
test('renders mentions summary', async () => {
commentsModel.setState({
drafts: {
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 b65c827..934fd65 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
@@ -370,6 +370,9 @@
@state()
private revertingChange?: ChangeInfo;
+ @state()
+ isFlowsEnabled = false;
+
// Private but used in tests.
@state()
scrollCommentId?: UrlEncodedCommentId;
@@ -626,7 +629,12 @@
changeNum => {
// The change view is tied to a specific change number, so don't update
// changeNum to undefined and only set it once.
- if (changeNum && !this.changeNum) this.changeNum = changeNum;
+ if (changeNum && !this.changeNum) {
+ this.changeNum = changeNum;
+ this.restApiService.getIfFlowsIsEnabled(this.changeNum).then(res => {
+ this.isFlowsEnabled = res?.enabled ?? false;
+ });
+ }
}
);
subscribe(
@@ -1381,7 +1389,8 @@
`
)}
${when(
- this.flagService.isEnabled(KnownExperimentId.SHOW_FLOWS_TAB),
+ this.flagService.isEnabled(KnownExperimentId.SHOW_FLOWS_TAB) &&
+ this.isFlowsEnabled,
() => html`
<md-secondary-tab
data-name=${Tab.FLOWS}
@@ -1649,7 +1658,7 @@
}
// Private but used in tests.
- handleCommitMessageSave(e: EditableContentSaveEvent) {
+ async handleCommitMessageSave(e: EditableContentSaveEvent) {
assertIsDefined(this.change, 'change');
assertIsDefined(this.changeNum, 'changeNum');
// to prevent 2 requests at the same time
@@ -1658,6 +1667,19 @@
const message = e.detail.content.replace(TRAILING_WHITESPACE_REGEX, '');
const committerEmail = e.detail.committerEmail;
+ // The BEFORE event handlers are async and can potentially block
+ // the message edit from going through. By contrast, the second
+ // set of event handlers always fire (and should probably fire
+ // after the message has been saved successfully, but the current
+ // behavior is what it is).
+ if (
+ !(await this.getPluginLoader().jsApiService.handleBeforeCommitMessage(
+ this.change,
+ message
+ ))
+ )
+ return;
+
this.getPluginLoader().jsApiService.handleCommitMessage(
this.change,
message
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 644cf02..0409f80 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
@@ -264,6 +264,9 @@
stubRestApi('getAccount').returns(
Promise.resolve(createAccountDetailWithId(5))
);
+ stubRestApi('getIfFlowsIsEnabled').returns(
+ Promise.resolve({enabled: true})
+ );
stubRestApi('getDiffComments').returns(Promise.resolve({}));
stubRestApi('getDiffDrafts').returns(Promise.resolve({}));
@@ -475,9 +478,11 @@
});
test('renders flows tab if experiment is enabled', async () => {
+ element.isFlowsEnabled = true;
stubFlags('isEnabled').returns(true);
element.requestUpdate();
await element.updateComplete;
+ await waitUntil(() => !!element.isFlowsEnabled);
queryAndAssert(element, '[data-name="flows"]');
});
@@ -1039,15 +1044,17 @@
});
assertIsDefined(element.commitMessageEditor);
- element.handleCommitMessageSave(
+ await element.handleCommitMessageSave(
mockEvent('test \n test ', committerEmail)
);
assert.equal(putStub.lastCall.args[1], 'test\n test');
element.commitMessageEditor.disabled = false;
- element.handleCommitMessageSave(mockEvent(' test\ntest', committerEmail));
+ await element.handleCommitMessageSave(
+ mockEvent(' test\ntest', committerEmail)
+ );
assert.equal(putStub.lastCall.args[1], ' test\ntest');
element.commitMessageEditor.disabled = false;
- element.handleCommitMessageSave(
+ await element.handleCommitMessageSave(
mockEvent('\n\n\n\n\n\n\n\n', committerEmail)
);
assert.equal(putStub.lastCall.args[1], '\n\n\n\n\n\n\n\n');
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-rebase-dialog/gr-confirm-rebase-dialog.ts b/polygerrit-ui/app/elements/change/gr-confirm-rebase-dialog/gr-confirm-rebase-dialog.ts
index 67603ce..fd44c59 100644
--- a/polygerrit-ui/app/elements/change/gr-confirm-rebase-dialog/gr-confirm-rebase-dialog.ts
+++ b/polygerrit-ui/app/elements/change/gr-confirm-rebase-dialog/gr-confirm-rebase-dialog.ts
@@ -4,7 +4,7 @@
* SPDX-License-Identifier: Apache-2.0
*/
import '../../shared/gr-account-chip/gr-account-chip';
-import {css, html, LitElement, PropertyValues} from 'lit';
+import {css, html, LitElement} from 'lit';
import {customElement, property, query, state} from 'lit/decorators.js';
import {when} from 'lit/directives/when.js';
import {
@@ -179,15 +179,6 @@
this.loadCommitterEmailDropdownItems();
}
- override willUpdate(changedProperties: PropertyValues): void {
- if (
- changedProperties.has('rebaseOnCurrent') ||
- changedProperties.has('hasParent')
- ) {
- this.updateSelectedOption();
- }
- }
-
static override get styles() {
return [
formStyles,
@@ -221,10 +212,10 @@
display: flex;
align-items: center;
gap: var(--spacing-m);
- margin-top: var(--spacing-l);
+ margin-top: var(--spacing-xl);
}
.rebaseCheckbox {
- margin-top: var(--spacing-m);
+ margin-top: var(--spacing-xl);
}
.rebaseOnBehalfMsg {
margin-top: var(--spacing-m);
@@ -232,6 +223,10 @@
.rebaseWithCommitterEmail {
margin-top: var(--spacing-m);
}
+ .main {
+ margin-top: var(--spacing-xl);
+ margin-bottom: var(--spacing-xl);
+ }
@media screen and (max-width: 50em) {
#confirmDialog {
height: 90vh;
@@ -252,8 +247,11 @@
>
<div class="header" slot="header">Confirm rebase</div>
<div class="main" slot="main">
- ${when(this.loading, () => html`<div>Loading...</div>`)}
- ${when(!this.loading, () => this.renderRebaseDialog())}
+ ${when(
+ this.loading,
+ () => html`<div>Loading...</div>`,
+ () => this.renderRebaseDialog()
+ )}
</div>
</gr-dialog>
`;
@@ -265,7 +263,12 @@
class="rebaseOption loading"
?hidden=${!this.displayParentOption() || this.loading}
>
- <md-radio id="rebaseOnParentInput" name="rebaseOptions"> </md-radio>
+ <md-radio
+ id="rebaseOnParentInput"
+ name="rebaseOptions"
+ ?checked=${this.displayParentOption()}
+ >
+ </md-radio>
<label id="rebaseOnParentLabel" for="rebaseOnParentInput">
Rebase on parent change
</label>
@@ -288,6 +291,7 @@
<md-radio
id="rebaseOnTipInput"
name="rebaseOptions"
+ ?checked=${!this.displayParentOption() && this.displayTipOption()}
?disabled=${!this.displayTipOption()}
>
</md-radio>
@@ -310,6 +314,7 @@
<md-radio
id="rebaseOnOtherInput"
name="rebaseOptions"
+ ?checked=${!this.displayParentOption() && !this.displayTipOption()}
@click=${this.handleRebaseOnOther}
>
</md-radio>
@@ -566,25 +571,6 @@
private handleEnterChangeNumberClick() {
if (this.rebaseOnOtherInput) this.rebaseOnOtherInput.checked = true;
}
-
- /**
- * Sets the default radio button based on the state of the app and
- * the corresponding value to be submitted.
- */
- private updateSelectedOption() {
- const {rebaseOnCurrent, hasParent} = this;
- if (rebaseOnCurrent === undefined || hasParent === undefined) {
- return;
- }
-
- if (this.displayParentOption()) {
- if (this.rebaseOnParentInput) this.rebaseOnParentInput.checked = true;
- } else if (this.displayTipOption()) {
- if (this.rebaseOnTipInput) this.rebaseOnTipInput.checked = true;
- } else {
- if (this.rebaseOnOtherInput) this.rebaseOnOtherInput.checked = true;
- }
- }
}
declare global {
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-rebase-dialog/gr-confirm-rebase-dialog_test.ts b/polygerrit-ui/app/elements/change/gr-confirm-rebase-dialog/gr-confirm-rebase-dialog_test.ts
index c2965cf..30cf475 100644
--- a/polygerrit-ui/app/elements/change/gr-confirm-rebase-dialog/gr-confirm-rebase-dialog_test.ts
+++ b/polygerrit-ui/app/elements/change/gr-confirm-rebase-dialog/gr-confirm-rebase-dialog_test.ts
@@ -63,7 +63,7 @@
<md-radio
id="rebaseOnParentInput"
name="rebaseOptions"
- tabindex="0"
+ tabindex="-1"
>
</md-radio>
<label for="rebaseOnParentInput" id="rebaseOnParentLabel">
@@ -81,7 +81,7 @@
disabled=""
id="rebaseOnTipInput"
name="rebaseOptions"
- tabindex="0"
+ tabindex="-1"
>
</md-radio>
<label for="rebaseOnTipInput" id="rebaseOnTipLabel">
@@ -93,7 +93,12 @@
Change is up to date with the target branch already (test)
</div>
<div class="rebaseOption" id="rebaseOnOther">
- <md-radio id="rebaseOnOtherInput" name="rebaseOptions" tabindex="0">
+ <md-radio
+ checked=""
+ id="rebaseOnOtherInput"
+ name="rebaseOptions"
+ tabindex="0"
+ >
</md-radio>
<label for="rebaseOnOtherInput" id="rebaseOnOtherLabel">
Rebase on a specific change, ref, or commit
@@ -428,7 +433,7 @@
);
assert.deepEqual((fireStub.lastCall.args[0] as CustomEvent).detail, {
allowConflicts: false,
- base: '123',
+ base: '',
rebaseChain: true,
onBehalfOfUploader: true,
committerEmail: null,
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-revert-dialog/gr-confirm-revert-dialog.ts b/polygerrit-ui/app/elements/change/gr-confirm-revert-dialog/gr-confirm-revert-dialog.ts
index fd9204c..52ae1c2 100644
--- a/polygerrit-ui/app/elements/change/gr-confirm-revert-dialog/gr-confirm-revert-dialog.ts
+++ b/polygerrit-ui/app/elements/change/gr-confirm-revert-dialog/gr-confirm-revert-dialog.ts
@@ -24,6 +24,8 @@
import {formStyles} from '../../../styles/form-styles';
import {GrValidationOptions} from '../gr-validation-options/gr-validation-options';
import {GrAutogrowTextarea} from '../../shared/gr-autogrow-textarea/gr-autogrow-textarea';
+import '@material/web/radio/radio';
+import {materialStyles} from '../../../styles/gr-material-styles';
const ERR_COMMIT_NOT_FOUND = 'Unable to find the commit hash of this change.';
const SPECIFY_REASON_STRING = '<MUST SPECIFY REASON HERE>';
@@ -85,6 +87,7 @@
return [
formStyles,
sharedStyles,
+ materialStyles,
css`
:host {
display: block;
@@ -101,6 +104,8 @@
.revertSubmissionLayout {
display: flex;
align-items: center;
+ margin-top: var(--spacing-l);
+ margin-bottom: var(--spacing-m);
}
.label {
margin-left: var(--spacing-m);
@@ -121,6 +126,9 @@
label[for='messageInput'] {
margin-top: var(--spacing-m);
}
+ gr-validation-options {
+ margin-bottom: var(--spacing-m);
+ }
`,
];
}
@@ -140,13 +148,13 @@
${this.showRevertSubmission
? html`
<div class="revertSubmissionLayout">
- <input
- name="revertOptions"
- type="radio"
+ <md-radio
id="revertSingleChange"
- @change=${() => this.handleRevertSingleChangeClicked()}
+ name="revertOptions"
?checked=${this.computeIfSingleRevert()}
- />
+ @change=${() => this.handleRevertSingleChangeClicked()}
+ >
+ </md-radio>
<label
for="revertSingleChange"
class="label revertSingleChange"
@@ -155,13 +163,13 @@
</label>
</div>
<div class="revertSubmissionLayout">
- <input
- name="revertOptions"
- type="radio"
+ <md-radio
id="revertSubmission"
+ name="revertOptions"
+ ?checked=${this.computeIfRevertSubmission()}
@change=${() => this.handleRevertSubmissionClicked()}
- .checked=${this.computeIfRevertSubmission()}
- />
+ >
+ </md-radio>
<label for="revertSubmission" class="label revertSubmission">
Revert entire submission (${this.changesCount} Changes)
</label>
diff --git a/polygerrit-ui/app/elements/change/gr-flows/gr-create-flow.ts b/polygerrit-ui/app/elements/change/gr-flows/gr-create-flow.ts
index d2656d0..f152806 100644
--- a/polygerrit-ui/app/elements/change/gr-flows/gr-create-flow.ts
+++ b/polygerrit-ui/app/elements/change/gr-flows/gr-create-flow.ts
@@ -18,6 +18,7 @@
import {MdOutlinedTextField} from '@material/web/textfield/outlined-text-field';
import {resolve} from '../../../models/dependency';
import {configModelToken} from '../../../models/config/config-model';
+import {flowsModelToken} from '../../../models/flows/flows-model';
import {subscribe} from '../../lit/subscription-controller';
import {throwingErrorCallback} from '../../shared/gr-rest-api-interface/gr-rest-apis/gr-rest-api-helper';
import {
@@ -60,6 +61,8 @@
private readonly getConfigModel = resolve(this, configModelToken);
+ private readonly getFlowsModel = resolve(this, flowsModelToken);
+
private readonly projectSuggestions: SuggestionProvider = (
predicate,
expression
@@ -347,17 +350,12 @@
return {condition: stage.condition};
}),
};
- await this.restApiService.createFlow(this.changeNum, flowInput, e => {
- console.error(e);
- });
+ await this.getFlowsModel().createFlow(flowInput);
this.stages = [];
this.currentCondition = '';
this.currentAction = '';
this.currentParameter = '';
this.loading = false;
- this.dispatchEvent(
- new CustomEvent('flow-created', {bubbles: true, composed: true})
- );
}
}
diff --git a/polygerrit-ui/app/elements/change/gr-flows/gr-create-flow_test.ts b/polygerrit-ui/app/elements/change/gr-flows/gr-create-flow_test.ts
index 1640709..ac71093 100644
--- a/polygerrit-ui/app/elements/change/gr-flows/gr-create-flow_test.ts
+++ b/polygerrit-ui/app/elements/change/gr-flows/gr-create-flow_test.ts
@@ -7,21 +7,20 @@
import './gr-create-flow';
import {assert, fixture, html} from '@open-wc/testing';
import {GrCreateFlow} from './gr-create-flow';
-import {
- mockPromise,
- queryAll,
- queryAndAssert,
- stubRestApi,
-} from '../../../test/test-utils';
+import {queryAll, queryAndAssert} from '../../../test/test-utils';
import {NumericChangeId} from '../../../types/common';
import {GrButton} from '../../shared/gr-button/gr-button';
import {MdOutlinedTextField} from '@material/web/textfield/outlined-text-field';
import {GrSearchAutocomplete} from '../../core/gr-search-autocomplete/gr-search-autocomplete';
+import {FlowsModel, flowsModelToken} from '../../../models/flows/flows-model';
+import {testResolver} from '../../../test/common-test-setup';
suite('gr-create-flow tests', () => {
let element: GrCreateFlow;
+ let flowsModel: FlowsModel;
setup(async () => {
+ flowsModel = testResolver(flowsModelToken);
element = await fixture<GrCreateFlow>(
html`<gr-create-flow></gr-create-flow>`
);
@@ -122,7 +121,7 @@
});
test('creates a flow with one stage', async () => {
- const createFlowStub = stubRestApi('createFlow').returns(mockPromise());
+ const createFlowStub = sinon.stub(flowsModel, 'createFlow');
const searchAutocomplete = queryAndAssert<GrSearchAutocomplete>(
element,
@@ -146,7 +145,7 @@
await element.updateComplete;
assert.isTrue(createFlowStub.calledOnce);
- const flowInput = createFlowStub.lastCall.args[1];
+ const flowInput = createFlowStub.lastCall.args[0];
assert.deepEqual(flowInput.stage_expressions, [
{
condition:
@@ -157,7 +156,7 @@
});
test('creates a flow with parameters', async () => {
- const createFlowStub = stubRestApi('createFlow').returns(mockPromise());
+ const createFlowStub = sinon.stub(flowsModel, 'createFlow');
const searchAutocomplete = queryAndAssert<GrSearchAutocomplete>(
element,
@@ -188,7 +187,7 @@
await element.updateComplete;
assert.isTrue(createFlowStub.calledOnce);
- const flowInput = createFlowStub.lastCall.args[1];
+ const flowInput = createFlowStub.lastCall.args[0];
assert.deepEqual(flowInput.stage_expressions, [
{
condition:
@@ -199,7 +198,7 @@
});
test('creates a flow with multiple stages', async () => {
- const createFlowStub = stubRestApi('createFlow').returns(mockPromise());
+ const createFlowStub = sinon.stub(flowsModel, 'createFlow');
const searchAutocomplete = queryAndAssert<GrSearchAutocomplete>(
element,
@@ -238,7 +237,7 @@
await element.updateComplete;
assert.isTrue(createFlowStub.calledOnce);
- const flowInput = createFlowStub.lastCall.args[1];
+ const flowInput = createFlowStub.lastCall.args[0];
assert.deepEqual(flowInput.stage_expressions, [
{
condition:
@@ -254,7 +253,7 @@
});
test('create flow with added stages and current input', async () => {
- const createFlowStub = stubRestApi('createFlow').returns(mockPromise());
+ const createFlowStub = sinon.stub(flowsModel, 'createFlow');
const searchAutocomplete = queryAndAssert<GrSearchAutocomplete>(
element,
@@ -290,7 +289,7 @@
await element.updateComplete;
assert.isTrue(createFlowStub.calledOnce);
- const flowInput = createFlowStub.lastCall.args[1];
+ const flowInput = createFlowStub.lastCall.args[0];
assert.deepEqual(flowInput.stage_expressions, [
{
condition:
diff --git a/polygerrit-ui/app/elements/change/gr-flows/gr-flows.ts b/polygerrit-ui/app/elements/change/gr-flows/gr-flows.ts
index 8136722..e272eda 100644
--- a/polygerrit-ui/app/elements/change/gr-flows/gr-flows.ts
+++ b/polygerrit-ui/app/elements/change/gr-flows/gr-flows.ts
@@ -11,12 +11,13 @@
import {changeModelToken} from '../../../models/change/change-model';
import {subscribe} from '../../lit/subscription-controller';
import {FlowInfo, FlowStageState} from '../../../api/rest-api';
-
-import {getAppContext} from '../../../services/app-context';
+import {flowsModelToken} from '../../../models/flows/flows-model';
import {NumericChangeId} from '../../../types/common';
import './gr-create-flow';
import {when} from 'lit/directives/when.js';
import '../../shared/gr-dialog/gr-dialog';
+import '@material/web/select/filled-select';
+import '@material/web/select/select-option';
const iconForFlowStageState = (status: FlowStageState) => {
switch (status) {
@@ -46,9 +47,11 @@
@state() private flowIdToDelete?: string;
+ @state() private statusFilter: FlowStageState | 'all' = 'all';
+
private readonly getChangeModel = resolve(this, changeModelToken);
- private readonly restApiService = getAppContext().restApiService;
+ private readonly getFlowsModel = resolve(this, flowsModelToken);
static override get styles() {
return [
@@ -132,23 +135,27 @@
() => this.getChangeModel().changeNum$,
changeNum => {
this.changeNum = changeNum;
- this.loadFlows();
+ }
+ );
+ subscribe(
+ this,
+ () => this.getFlowsModel().flows$,
+ flows => {
+ this.flows = flows;
+ }
+ );
+ subscribe(
+ this,
+ () => this.getFlowsModel().loading$,
+ loading => {
+ this.loading = loading;
}
);
}
- async loadFlows() {
- if (!this.changeNum) return;
- this.loading = true;
- const flows = await this.restApiService.listFlows(this.changeNum);
- this.flows = flows ?? [];
- this.loading = false;
- }
-
private async deleteFlow() {
- if (!this.changeNum || !this.flowIdToDelete) return;
- await this.restApiService.deleteFlow(this.changeNum, this.flowIdToDelete);
- await this.loadFlows();
+ if (!this.flowIdToDelete) return;
+ await this.getFlowsModel().deleteFlow(this.flowIdToDelete);
this.closeConfirmDialog();
}
@@ -166,10 +173,7 @@
return html`
<div class="container">
<h2 class="main-heading">Create new flow</h2>
- <gr-create-flow
- .changeNum=${this.changeNum}
- @flow-created=${this.loadFlows}
- ></gr-create-flow>
+ <gr-create-flow .changeNum=${this.changeNum}></gr-create-flow>
<hr />
${this.renderFlowsList()}
</div>
@@ -210,13 +214,19 @@
if (this.flows.length === 0) {
return html`<p>No flows found for this change.</p>`;
}
+ const filteredFlows = this.flows.filter(flow => {
+ if (this.statusFilter === 'all') return true;
+ const lastStage = flow.stages[flow.stages.length - 1];
+ return lastStage.state === this.statusFilter;
+ });
+
return html`
<div>
<div class="heading-with-button">
<h2 class="main-heading">Existing Flows</h2>
<gr-button
link
- @click=${this.loadFlows}
+ @click=${() => this.getFlowsModel().reload()}
aria-label="Refresh flows"
title="Refresh flows"
class="refresh"
@@ -224,7 +234,26 @@
<gr-icon icon="refresh"></gr-icon>
</gr-button>
</div>
- ${this.flows.map(
+ <md-filled-select
+ label="Filter by status"
+ @request-selection=${(e: CustomEvent) => {
+ this.statusFilter = (e.target as HTMLSelectElement).value as
+ | FlowStageState
+ | 'all';
+ }}
+ >
+ <md-select-option value="all">
+ <div slot="headline">All</div>
+ </md-select-option>
+ ${Object.values(FlowStageState).map(
+ status => html`
+ <md-select-option value=${status}>
+ <div slot="headline">${status}</div>
+ </md-select-option>
+ `
+ )}
+ </md-filled-select>
+ ${filteredFlows.map(
(flow: FlowInfo) => html`
<div class="flow">
<div class="flow-header">
diff --git a/polygerrit-ui/app/elements/change/gr-flows/gr-flows_test.ts b/polygerrit-ui/app/elements/change/gr-flows/gr-flows_test.ts
index cb14d9e..b21b090 100644
--- a/polygerrit-ui/app/elements/change/gr-flows/gr-flows_test.ts
+++ b/polygerrit-ui/app/elements/change/gr-flows/gr-flows_test.ts
@@ -8,19 +8,27 @@
import {assert, fixture, html} from '@open-wc/testing';
import {GrFlows} from './gr-flows';
import {FlowInfo, FlowStageState, Timestamp} from '../../../api/rest-api';
-import {queryAndAssert, stubRestApi} from '../../../test/test-utils';
+import {queryAndAssert} from '../../../test/test-utils';
import {NumericChangeId} from '../../../types/common';
-import {GrCreateFlow} from './gr-create-flow';
import sinon from 'sinon';
import {GrButton} from '../../shared/gr-button/gr-button';
import {GrDialog} from '../../shared/gr-dialog/gr-dialog';
+import {FlowsModel, flowsModelToken} from '../../../models/flows/flows-model';
+import {testResolver} from '../../../test/common-test-setup';
suite('gr-flows tests', () => {
let element: GrFlows;
let clock: sinon.SinonFakeTimers;
+ let flowsModel: FlowsModel;
setup(async () => {
clock = sinon.useFakeTimers();
+
+ flowsModel = testResolver(flowsModelToken);
+ // The model is created by the DI system. The test setup replaces the real
+ // model with a mock. To prevent real API calls, we stub the reload method.
+ sinon.stub(flowsModel, 'reload');
+
element = await fixture<GrFlows>(html`<gr-flows></gr-flows>`);
element['changeNum'] = 123 as NumericChangeId;
await element.updateComplete;
@@ -31,8 +39,7 @@
});
test('renders create flow component and no flows', async () => {
- stubRestApi('listFlows').returns(Promise.resolve([]));
- await element['loadFlows']();
+ flowsModel.setState({flows: [], loading: false});
await element.updateComplete;
assert.shadowDom.equal(
element,
@@ -85,8 +92,7 @@
],
},
];
- stubRestApi('listFlows').returns(Promise.resolve(flows));
- await element['loadFlows']();
+ flowsModel.setState({flows, loading: false});
await element.updateComplete;
// prettier formats the spacing for "last evaluated" incorrectly
@@ -108,6 +114,23 @@
<gr-icon icon="refresh"></gr-icon>
</gr-button>
</div>
+ <md-filled-select label="Filter by status">
+ <md-select-option value="all">
+ <div slot="headline">All</div>
+ </md-select-option>
+ <md-select-option value="DONE">
+ <div slot="headline">DONE</div>
+ </md-select-option>
+ <md-select-option value="FAILED">
+ <div slot="headline">FAILED</div>
+ </md-select-option>
+ <md-select-option value="PENDING">
+ <div slot="headline">PENDING</div>
+ </md-select-option>
+ <md-select-option value="TERMINATED">
+ <div slot="headline">TERMINATED</div>
+ </md-select-option>
+ </md-filled-select>
<div class="flow">
<div class="flow-header">
<gr-button link title="Delete flow">
@@ -205,6 +228,7 @@
'aria-disabled',
'role',
'tabindex',
+ 'md-menu-item',
],
}
);
@@ -224,11 +248,8 @@
],
},
];
- stubRestApi('listFlows').returns(Promise.resolve(flows));
- const deleteFlowStub = sinon
- .stub(element['restApiService'], 'deleteFlow')
- .returns(Promise.resolve(new Response()));
- await element['loadFlows']();
+ const deleteFlowStub = sinon.stub(flowsModel, 'deleteFlow');
+ flowsModel.setState({flows, loading: false});
await element.updateComplete;
const deleteButton = queryAndAssert<GrButton>(element, '.flow gr-button');
@@ -246,7 +267,7 @@
confirmButton.click();
await element.updateComplete;
- assert.isTrue(deleteFlowStub.calledOnceWith(123, 'flow1'));
+ assert.isTrue(deleteFlowStub.calledOnceWith('flow1'));
});
test('cancel deleting a flow', async () => {
@@ -263,11 +284,8 @@
],
},
];
- stubRestApi('listFlows').returns(Promise.resolve(flows));
- const deleteFlowStub = sinon
- .stub(element['restApiService'], 'deleteFlow')
- .returns(Promise.resolve(new Response()));
- await element['loadFlows']();
+ const deleteFlowStub = sinon.stub(flowsModel, 'deleteFlow');
+ flowsModel.setState({flows, loading: false});
await element.updateComplete;
const deleteButton = queryAndAssert<GrButton>(element, '.flow gr-button');
@@ -289,44 +307,19 @@
assert.isFalse(dialog.open);
});
- test('reloads flows on flow-created event', async () => {
- const listFlowsStub = stubRestApi('listFlows').returns(Promise.resolve([]));
- await element['loadFlows']();
- await element.updateComplete;
-
- assert.isTrue(listFlowsStub.calledOnce);
-
- const createFlow = queryAndAssert<GrCreateFlow>(element, 'gr-create-flow');
- createFlow.dispatchEvent(
- new CustomEvent('flow-created', {bubbles: true, composed: true})
- );
-
- await element.updateComplete;
-
- assert.isTrue(listFlowsStub.calledTwice);
- });
-
test('refreshes flows on button click', async () => {
- const flows: FlowInfo[] = [
- {
- uuid: 'flow1',
- owner: {name: 'owner1'},
- created: '2025-01-01T10:00:00.000Z' as Timestamp,
- stages: [
- {
- expression: {condition: 'label:Code-Review=+1'},
- state: FlowStageState.DONE,
- },
- ],
- },
- ];
- const listFlowsStub = stubRestApi('listFlows').returns(
- Promise.resolve(flows)
- );
- await element.loadFlows();
+ const flow = {
+ uuid: 'flow1',
+ owner: {name: 'owner1'},
+ created: '2025-01-01T10:00:00.000Z' as Timestamp,
+ stages: [],
+ } as FlowInfo;
+ flowsModel.setState({flows: [flow], loading: false});
await element.updateComplete;
- assert.isTrue(listFlowsStub.calledOnce);
+ const reloadStub = flowsModel.reload as sinon.SinonStub;
+ reloadStub.resetHistory();
+
const refreshButton = queryAndAssert<GrButton>(
element,
'.heading-with-button gr-button'
@@ -334,6 +327,111 @@
refreshButton.click();
await element.updateComplete;
- assert.isTrue(listFlowsStub.calledTwice);
+ assert.isTrue(reloadStub.calledOnce);
+ });
+
+ suite('filter', () => {
+ const flows: FlowInfo[] = [
+ {
+ uuid: 'flow-done',
+ owner: {name: 'owner1'},
+ created: '2025-01-01T10:00:00.000Z' as Timestamp,
+ stages: [
+ {expression: {condition: 'cond-done'}, state: FlowStageState.DONE},
+ ],
+ },
+ {
+ uuid: 'flow-pending',
+ owner: {name: 'owner2'},
+ created: '2025-01-02T10:00:00.000Z' as Timestamp,
+ stages: [
+ {
+ expression: {condition: 'cond-pending'},
+ state: FlowStageState.PENDING,
+ },
+ ],
+ },
+ {
+ uuid: 'flow-failed',
+ owner: {name: 'owner3'},
+ created: '2025-01-03T10:00:00.000Z' as Timestamp,
+ stages: [
+ {
+ expression: {condition: 'cond-failed'},
+ state: FlowStageState.FAILED,
+ },
+ ],
+ },
+ {
+ uuid: 'flow-terminated',
+ owner: {name: 'owner4'},
+ created: '2025-01-04T10:00:00.000Z' as Timestamp,
+ stages: [
+ {
+ expression: {condition: 'cond-terminated'},
+ state: FlowStageState.TERMINATED,
+ },
+ ],
+ },
+ ];
+
+ setup(async () => {
+ flowsModel.setState({flows, loading: false});
+ await element.updateComplete;
+ });
+
+ test('shows all flows by default', () => {
+ const flowElements = element.shadowRoot!.querySelectorAll('.flow');
+ assert.equal(flowElements.length, 4);
+ });
+
+ test('filters by DONE', async () => {
+ element['statusFilter'] = FlowStageState.DONE;
+ await element.updateComplete;
+
+ const flowElements = element.shadowRoot!.querySelectorAll('.flow');
+ assert.equal(flowElements.length, 1);
+ assert.include(flowElements[0].textContent, 'cond-done');
+ });
+
+ test('filters by PENDING', async () => {
+ element['statusFilter'] = FlowStageState.PENDING;
+ await element.updateComplete;
+
+ const flowElements = element.shadowRoot!.querySelectorAll('.flow');
+ assert.equal(flowElements.length, 1);
+ assert.include(flowElements[0].textContent, 'cond-pending');
+ });
+
+ test('filters by FAILED', async () => {
+ element['statusFilter'] = FlowStageState.FAILED;
+ await element.updateComplete;
+
+ const flowElements = element.shadowRoot!.querySelectorAll('.flow');
+ assert.equal(flowElements.length, 1);
+ assert.include(flowElements[0].textContent, 'cond-failed');
+ });
+
+ test('filters by TERMINATED', async () => {
+ element['statusFilter'] = FlowStageState.TERMINATED;
+ await element.updateComplete;
+
+ const flowElements = element.shadowRoot!.querySelectorAll('.flow');
+ assert.equal(flowElements.length, 1);
+ assert.include(flowElements[0].textContent, 'cond-terminated');
+ });
+
+ test('shows all when filter is changed to all', async () => {
+ element['statusFilter'] = FlowStageState.DONE;
+ await element.updateComplete;
+ let flowElements = element.shadowRoot!.querySelectorAll('.flow');
+ assert.equal(flowElements.length, 1);
+
+ element['statusFilter'] = 'all';
+ await element.updateComplete;
+
+ flowElements = element.shadowRoot!.querySelectorAll('.flow');
+ assert.equal(flowElements.length, 4);
+ });
});
});
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 623fe40..04c8618 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
@@ -1425,6 +1425,7 @@
);
if (!this.ccs.find(cc => getUserId(cc) === getUserId(accountToMove))) {
this.ccs = [...this.ccs, accountToMove];
+ this.reviewersMutated = true;
}
} else {
this.ccs = this.ccs.filter(
@@ -1434,6 +1435,7 @@
!this.reviewers.find(r => getUserId(r) === getUserId(accountToMove))
) {
this.reviewers = [...this.reviewers, accountToMove];
+ this.reviewersMutated = true;
}
}
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 8a58551..3963888 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
@@ -47,6 +47,7 @@
countRunningRunsForLabel,
} from '../../checks/gr-checks-util';
import {subscribe} from '../../lit/subscription-controller';
+import {when} from 'lit/directives/when.js';
/**
* @attr {Boolean} suppress-title - hide titles, currently for hovercard view
@@ -153,7 +154,6 @@
const submit_requirements = orderSubmitRequirements(
getRequirements(this.change)
);
- if (submit_requirements.length === 0) return nothing;
const requirementKey = (req: SubmitRequirementResultInfo, index: number) =>
`${index}-${req.name}`;
@@ -162,34 +162,44 @@
id="submit-requirements-caption"
>
Submit Requirements
+ ${submit_requirements.length === 0 ? '(Loading...)' : ''}
</h3>
- <table class="requirements" aria-labelledby="submit-requirements-caption">
- <thead hidden>
- <tr>
- <th>Status</th>
- <th>Name</th>
- <th>Votes</th>
- </tr>
- </thead>
- <tbody>
- ${repeat(submit_requirements, requirementKey, (requirement, index) =>
- this.renderRequirement(requirement, index)
- )}
- </tbody>
- </table>
- ${this.disableHovercards
- ? ''
- : submit_requirements.map(
- (requirement, index) => html`
- <gr-submit-requirement-hovercard
- for="requirement-${index}-${charsOnly(requirement.name)}"
- .requirement=${requirement}
- .change=${this.change}
- .account=${this.account}
- .mutable=${this.mutable ?? false}
- ></gr-submit-requirement-hovercard>
- `
- )}`;
+ ${when(
+ submit_requirements.length !== 0,
+ () => html`<table
+ class="requirements"
+ aria-labelledby="submit-requirements-caption"
+ >
+ <thead hidden>
+ <tr>
+ <th>Status</th>
+ <th>Name</th>
+ <th>Votes</th>
+ </tr>
+ </thead>
+ <tbody>
+ ${repeat(
+ submit_requirements,
+ requirementKey,
+ (requirement, index) =>
+ this.renderRequirement(requirement, index)
+ )}
+ </tbody>
+ </table>
+ ${this.disableHovercards
+ ? ''
+ : submit_requirements.map(
+ (requirement, index) => html`
+ <gr-submit-requirement-hovercard
+ for="requirement-${index}-${charsOnly(requirement.name)}"
+ .requirement=${requirement}
+ .change=${this.change}
+ .account=${this.account}
+ .mutable=${this.mutable ?? false}
+ ></gr-submit-requirement-hovercard>
+ `
+ )}`
+ )}`;
}
private renderRequirement(
diff --git a/polygerrit-ui/app/elements/change/gr-submit-requirements/gr-submit-requirements_test.ts b/polygerrit-ui/app/elements/change/gr-submit-requirements/gr-submit-requirements_test.ts
index 6812251..aee2017 100644
--- a/polygerrit-ui/app/elements/change/gr-submit-requirements/gr-submit-requirements_test.ts
+++ b/polygerrit-ui/app/elements/change/gr-submit-requirements/gr-submit-requirements_test.ts
@@ -23,24 +23,33 @@
SubmitRequirementResultInfo,
SubmitRequirementStatus,
} from '../../../api/rest-api';
-import {ParsedChangeInfo} from '../../../types/types';
+import {LoadingStatus, ParsedChangeInfo} from '../../../types/types';
import {RunStatus} from '../../../api/checks';
+import {testResolver} from '../../../test/common-test-setup';
+import {
+ ChangeModel,
+ changeModelToken,
+} from '../../../models/change/change-model';
suite('gr-submit-requirements tests', () => {
let element: GrSubmitRequirements;
let change: ParsedChangeInfo;
+ let changeModel: ChangeModel;
+
setup(async () => {
const submitRequirement: SubmitRequirementResultInfo = {
...createSubmitRequirementResultInfo(),
description: 'Test Description',
submittability_expression_result: createSubmitRequirementExpressionInfo(),
};
+ const submitRequirements: SubmitRequirementResultInfo[] = [
+ submitRequirement,
+ createNonApplicableSubmitRequirementResultInfo(),
+ ];
change = {
...createParsedChange(),
- submit_requirements: [
- submitRequirement,
- createNonApplicableSubmitRequirementResultInfo(),
- ],
+ submittable: false,
+ submit_requirements: submitRequirements,
labels: {
Verified: {
...createDetailedLabelInfo(),
@@ -54,12 +63,23 @@
},
};
const account = createAccountWithIdNameAndEmail();
+ changeModel = testResolver(changeModelToken);
+ changeModel.setState({
+ change,
+ submittabilityInfo: {
+ changeNum: change._number,
+ submitRequirements,
+ submittable: false,
+ },
+ loadingStatus: LoadingStatus.LOADED,
+ });
element = await fixture<GrSubmitRequirements>(
html`<gr-submit-requirements
.change=${change}
.account=${account}
></gr-submit-requirements>`
);
+ await element.updateComplete;
});
test('renders', () => {
diff --git a/polygerrit-ui/app/elements/change/gr-validation-options/gr-validation-options.ts b/polygerrit-ui/app/elements/change/gr-validation-options/gr-validation-options.ts
index de37852..6fdb74e 100644
--- a/polygerrit-ui/app/elements/change/gr-validation-options/gr-validation-options.ts
+++ b/polygerrit-ui/app/elements/change/gr-validation-options/gr-validation-options.ts
@@ -67,7 +67,7 @@
class="selectionLabel"
id=${option.name}
?checked=${!!this.isOptionSelected.get(option.name)}
- @click=${() => this.toggleCheckbox(option)}
+ @change=${() => this.toggleCheckbox(option)}
></md-checkbox>
<label for=${option.name}
>${capitalizeFirstLetter(option.description)}</label
diff --git a/polygerrit-ui/app/elements/checks/gr-checks-runs.ts b/polygerrit-ui/app/elements/checks/gr-checks-runs.ts
index 5f92d16..54f636e 100644
--- a/polygerrit-ui/app/elements/checks/gr-checks-runs.ts
+++ b/polygerrit-ui/app/elements/checks/gr-checks-runs.ts
@@ -59,6 +59,8 @@
import {when} from 'lit/directives/when.js';
import {changeViewModelToken} from '../../models/views/change';
import {formStyles} from '../../styles/form-styles';
+import '@material/web/radio/radio';
+import {materialStyles} from '../../styles/gr-material-styles';
@customElement('gr-checks-run')
export class GrChecksRun extends LitElement {
@@ -66,6 +68,7 @@
return [
formStyles,
sharedStyles,
+ materialStyles,
css`
:host {
display: block;
@@ -173,7 +176,7 @@
.attemptDetail {
/* This is thick-border (6) + spacing-m (8) + icon (20) + padding. */
padding-left: 39px;
- padding-top: var(--spacing-s);
+ padding-top: var(--spacing-l);
}
.attemptDetail input {
width: 14px;
@@ -320,14 +323,14 @@
.attempt=${attempt as number}
></gr-hovercard-run>`
)}
- <input
- type="radio"
+ <md-radio
id=${id}
name=${`${checkNameId}-attempt-choice`}
- .checked=${selected}
+ ?checked=${selected}
?disabled=${!selected && wasNotRun}
@change=${() => this.handleAttemptChange(attempt)}
- />
+ >
+ </md-radio>
<gr-icon
icon=${icon.name}
class=${icon.name}
diff --git a/polygerrit-ui/app/elements/checks/gr-checks-runs_test.ts b/polygerrit-ui/app/elements/checks/gr-checks-runs_test.ts
index 13a2545..871a598 100644
--- a/polygerrit-ui/app/elements/checks/gr-checks-runs_test.ts
+++ b/polygerrit-ui/app/elements/checks/gr-checks-runs_test.ts
@@ -53,7 +53,7 @@
role="switch"
>
<div>
- <gr-icon icon="chevron_left" class="expandIcon"></gr-icon>
+ <gr-icon class="expandIcon" icon="chevron_left"> </gr-icon>
</div>
</gr-button>
</gr-tooltip-content>
@@ -64,7 +64,8 @@
</div>
<div class="right">
<div class="message">
- Error while fetching results for test-plugin-name: <br />
+ Error while fetching results for test-plugin-name:
+ <br />
test-error-message
</div>
</div>
@@ -76,32 +77,32 @@
/>
<div class="expanded running">
<div class="sectionHeader">
- <gr-icon icon="expand_less" class="expandIcon"></gr-icon>
+ <gr-icon class="expandIcon" icon="expand_less"> </gr-icon>
<h3 class="heading-3">Running / Scheduled (2)</h3>
</div>
<div class="sectionRuns">
- <gr-checks-run></gr-checks-run>
- <gr-checks-run></gr-checks-run>
+ <gr-checks-run> </gr-checks-run>
+ <gr-checks-run> </gr-checks-run>
</div>
</div>
<div class="completed expanded">
<div class="sectionHeader">
- <gr-icon icon="expand_less" class="expandIcon"></gr-icon>
+ <gr-icon class="expandIcon" icon="expand_less"> </gr-icon>
<h3 class="heading-3">Completed (3)</h3>
</div>
<div class="sectionRuns">
- <gr-checks-run></gr-checks-run>
- <gr-checks-run></gr-checks-run>
- <gr-checks-run></gr-checks-run>
+ <gr-checks-run> </gr-checks-run>
+ <gr-checks-run> </gr-checks-run>
+ <gr-checks-run> </gr-checks-run>
</div>
</div>
<div class="expanded runnable">
<div class="sectionHeader">
- <gr-icon icon="expand_less" class="expandIcon"></gr-icon>
+ <gr-icon class="expandIcon" icon="expand_less"> </gr-icon>
<h3 class="heading-3">Not run (1)</h3>
</div>
<div class="sectionRuns">
- <gr-checks-run></gr-checks-run>
+ <gr-checks-run> </gr-checks-run>
</div>
</div>
`,
@@ -216,26 +217,28 @@
<gr-checks-attempt> </gr-checks-attempt>
</div>
<div class="right"></div>
+ </div>
+ <div class="attemptDetails" hidden="">
+ <div class="attemptDetail">
+ <md-radio
+ checked=""
+ id="attempt-latest"
+ name="fakeerrorfinderfinderfinderfinderfinderfinderfinder-attempt-choice"
+ tabindex="0"
+ >
+ </md-radio>
+ <gr-icon icon=""> </gr-icon>
+ <label for="attempt-latest"> Latest Attempt </label>
</div>
- <div class="attemptDetails" hidden="">
- <div class="attemptDetail">
- <input
- id="attempt-latest"
- name="fakeerrorfinderfinderfinderfinderfinderfinderfinder-attempt-choice"
- type="radio"
- />
- <gr-icon icon=""> </gr-icon>
- <label for="attempt-latest"> Latest Attempt </label>
- </div>
- <div class="attemptDetail">
- <input
- id="attempt-all"
- name="fakeerrorfinderfinderfinderfinderfinderfinderfinder-attempt-choice"
- type="radio"
- />
- <gr-icon icon=""> </gr-icon>
- <label for="attempt-all"> All Attempts </label>
- </div>
+ <div class="attemptDetail">
+ <md-radio
+ id="attempt-all"
+ name="fakeerrorfinderfinderfinderfinderfinderfinderfinder-attempt-choice"
+ tabindex="-1"
+ >
+ </md-radio>
+ <gr-icon icon=""> </gr-icon>
+ <label for="attempt-all"> All Attempts </label>
</div>
</div>
`
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 6471d1e..f348a80 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
@@ -10,9 +10,12 @@
import '../../shared/gr-tooltip-content/gr-tooltip-content';
import '../gr-default-editor/gr-default-editor';
import {navigationToken} from '../../core/gr-navigation/gr-navigation';
+import {pluginLoaderToken} from '../../shared/gr-js-api-interface/gr-plugin-loader';
import {
Base64FileContent,
+ ChangeInfo,
EditPreferencesInfo,
+ RevisionInfo,
RevisionPatchSetNum,
} from '../../../types/common';
import {ParsedChangeInfo} from '../../../types/types';
@@ -94,6 +97,8 @@
private readonly reporting = getAppContext().reportingService;
+ private readonly getPluginLoader = resolve(this, pluginLoaderToken);
+
private readonly getStorage = resolve(this, storageServiceToken);
private readonly getUserModel = resolve(this, userModelToken);
@@ -541,11 +546,27 @@
return;
}
assertIsDefined(this.change, 'change');
+
+ this.getPluginLoader().jsApiService.handlePublishEdit(
+ this.change as ChangeInfo,
+ this.getLatestRevision(this.change as ChangeInfo)
+ );
+
this.getChangeModel().navigateToChangeResetReload();
});
});
};
+ private getLatestRevision(change: ChangeInfo): RevisionInfo | null {
+ const patchNum = this.latestPatchsetNumber;
+ for (const rev of Object.values(change.revisions ?? {})) {
+ if (rev._number === patchNum) {
+ return rev;
+ }
+ }
+ return null;
+ }
+
private handleContentChange(e: CustomEvent<{value: string}>) {
this.storeTask = debounce(
this.storeTask,
diff --git a/polygerrit-ui/app/elements/settings/gr-change-table-editor/gr-change-table-editor.ts b/polygerrit-ui/app/elements/settings/gr-change-table-editor/gr-change-table-editor.ts
index 9e970e6..8fb6e88 100644
--- a/polygerrit-ui/app/elements/settings/gr-change-table-editor/gr-change-table-editor.ts
+++ b/polygerrit-ui/app/elements/settings/gr-change-table-editor/gr-change-table-editor.ts
@@ -82,7 +82,7 @@
id="numberCheckbox"
name="number"
?checked=${!!this.showNumber}
- @click=${this.handleNumberCheckboxClick}
+ @change=${this.handleNumberCheckboxClick}
></md-checkbox>
</td>
</tr>
@@ -100,7 +100,7 @@
id=${column}
name=${column}
?checked=${!this.computeIsColumnHidden(column)}
- @click=${this.handleTargetClick}
+ @change=${this.handleTargetClick}
></md-checkbox>
</td>
</tr>`;
diff --git a/polygerrit-ui/app/elements/settings/gr-change-table-editor/gr-change-table-editor_test.ts b/polygerrit-ui/app/elements/settings/gr-change-table-editor/gr-change-table-editor_test.ts
index b322d46..a0a747d 100644
--- a/polygerrit-ui/app/elements/settings/gr-change-table-editor/gr-change-table-editor_test.ts
+++ b/polygerrit-ui/app/elements/settings/gr-change-table-editor/gr-change-table-editor_test.ts
@@ -160,7 +160,7 @@
assert.equal(element.displayedColumns.length, displayedLength - 1);
});
- test('show item', async () => {
+ test('show and hide item', async () => {
element.displayedColumns = [
ColumnNames.STATUS,
ColumnNames.OWNER,
@@ -171,20 +171,35 @@
// trigger computation of enabled displayed columns
element.serverConfig = createServerInfo();
await element.updateComplete;
- const checkbox = queryAndAssert<MdCheckbox>(
+
+ const checkboxSubject = queryAndAssert<MdCheckbox>(
element,
'table tr:nth-child(2) md-checkbox'
);
- const isChecked = checkbox.checked;
- const displayedLength = element.displayedColumns.length;
- assert.isFalse(isChecked);
- const table = queryAndAssert<HTMLTableElement>(element, 'table');
- assert.equal(table.style.display, '');
+ assert.equal(checkboxSubject.name, 'Subject');
+ const checkboxOwner = queryAndAssert<MdCheckbox>(
+ element,
+ 'table tr:nth-child(3) md-checkbox'
+ );
+ assert.equal(checkboxOwner.name, 'Owner');
- checkbox.click();
+ assert.equal(element.displayedColumns.length, 5);
+ assert.isFalse(checkboxSubject.checked);
+ assert.isTrue(checkboxOwner.checked);
+
+ checkboxSubject.click();
await element.updateComplete;
- assert.equal(element.displayedColumns.length, displayedLength + 1);
+ assert.equal(element.displayedColumns.length, 6);
+ assert.isTrue(checkboxSubject.checked);
+ assert.isTrue(checkboxOwner.checked);
+
+ checkboxOwner.click();
+ await element.updateComplete;
+
+ assert.equal(element.displayedColumns.length, 5);
+ assert.isTrue(checkboxSubject.checked);
+ assert.isFalse(checkboxOwner.checked);
});
test('getDisplayedColumns', () => {
diff --git a/polygerrit-ui/app/elements/settings/gr-cla-view/gr-cla-view.ts b/polygerrit-ui/app/elements/settings/gr-cla-view/gr-cla-view.ts
index 566ed93..f43ea62 100644
--- a/polygerrit-ui/app/elements/settings/gr-cla-view/gr-cla-view.ts
+++ b/polygerrit-ui/app/elements/settings/gr-cla-view/gr-cla-view.ts
@@ -20,6 +20,7 @@
import {ifDefined} from 'lit/directives/if-defined.js';
import '@material/web/textfield/outlined-text-field';
import {materialStyles} from '../../../styles/gr-material-styles';
+import '@material/web/radio/radio';
declare global {
interface HTMLElementTagNameMap {
@@ -111,15 +112,15 @@
private renderAgreementsButton(item: ContributorAgreementInfo) {
return html`
<span class="contributorAgreementButton">
- <input
+ <md-radio
id="claNewAgreementsInput${item.name}"
name="claNewAgreementsRadio"
- type="radio"
data-name=${ifDefined(item.name)}
data-url=${ifDefined(item.url)}
- @click=${this.handleShowAgreement}
?disabled=${this.disableAgreements(item)}
- />
+ @change=${this.handleShowAgreement}
+ >
+ </md-radio>
<label id="claNewAgreementsLabel">${item.name}</label>
</span>
${this.renderAlreadySubmittedText(item)}
diff --git a/polygerrit-ui/app/elements/settings/gr-cla-view/gr-cla-view_test.ts b/polygerrit-ui/app/elements/settings/gr-cla-view/gr-cla-view_test.ts
index 37e21ec..b842027 100644
--- a/polygerrit-ui/app/elements/settings/gr-cla-view/gr-cla-view_test.ts
+++ b/polygerrit-ui/app/elements/settings/gr-cla-view/gr-cla-view_test.ts
@@ -132,31 +132,32 @@
<h1 class="heading-1">New Contributor Agreement</h1>
<h3 class="heading-3">Select an agreement type:</h3>
<span class="contributorAgreementButton">
- <input
+ <md-radio
data-name="Individual"
data-url="static/cla_individual.html"
id="claNewAgreementsInputIndividual"
name="claNewAgreementsRadio"
- type="radio"
- />
+ >
+ </md-radio>
<label id="claNewAgreementsLabel"> Individual </label>
</span>
<div class="agreementsUrl">test-description</div>
<span class="contributorAgreementButton">
- <input
+ <md-radio
data-name="CLA"
data-url="static/cla.html"
disabled=""
id="claNewAgreementsInputCLA"
name="claNewAgreementsRadio"
- type="radio"
- />
+ >
+ </md-radio>
<label id="claNewAgreementsLabel"> CLA </label>
</span>
<div class="alreadySubmittedText">Agreement already submitted.</div>
<div class="agreementsUrl">Contributor License Agreement</div>
</main>
- `
+ `,
+ {ignoreAttributes: ['tabindex']}
);
});
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 6308b12..df26cb8 100644
--- a/polygerrit-ui/app/elements/shared/gr-comment/gr-comment.ts
+++ b/polygerrit-ui/app/elements/shared/gr-comment/gr-comment.ts
@@ -95,6 +95,8 @@
} from '../../../services/suggestions/suggestions-service';
import {ResponseCode, SuggestionsProvider} from '../../../api/suggestions';
import {pluginLoaderToken} from '../gr-js-api-interface/gr-plugin-loader';
+import '@material/web/checkbox/checkbox';
+import {materialStyles} from '../../../styles/gr-material-styles';
// visible for testing
export const AUTO_SAVE_DEBOUNCE_DELAY_MS = 2000;
@@ -517,6 +519,7 @@
formStyles,
sharedStyles,
modalStyles,
+ materialStyles,
css`
:host {
display: block;
@@ -591,7 +594,7 @@
margin-left: var(--spacing-s);
}
/* just for a11y */
- input.show-hide {
+ md-checkbox.show-hide {
display: none;
}
label.show-hide {
@@ -874,12 +877,11 @@
return html`
<div class="show-hide" tabindex="0">
<label class="show-hide" aria-label=${ariaLabel}>
- <input
- type="checkbox"
+ <md-checkbox
class="show-hide"
?checked=${!!this.collapsed}
@change=${() => (this.collapsed = !this.collapsed)}
- />
+ ></md-checkbox>
<gr-icon icon=${icon} id="icon"></gr-icon>
</label>
</div>
@@ -1026,12 +1028,11 @@
<div class="leftActions">
<div class="action resolve">
<label>
- <input
- type="checkbox"
+ <md-checkbox
id="resolvedCheckbox"
- .checked=${!this.unresolved}
+ ?checked=${!this.unresolved}
@change=${this.handleToggleResolved}
- />
+ ></md-checkbox>
Resolved
</label>
</div>
@@ -1178,8 +1179,7 @@
return html`
<div class="action">
<label title=${tooltip} class="suggestEdit">
- <input
- type="checkbox"
+ <md-checkbox
id="generateSuggestCheckbox"
?checked=${this.generateSuggestion}
@change=${() => {
@@ -1213,7 +1213,7 @@
}
);
}}
- />
+ ></md-checkbox>
Attach AI-suggested fix
${when(
this.suggestionLoading,
diff --git a/polygerrit-ui/app/elements/shared/gr-comment/gr-comment_test.ts b/polygerrit-ui/app/elements/shared/gr-comment/gr-comment_test.ts
index 230b1b6..1f5a9eb 100644
--- a/polygerrit-ui/app/elements/shared/gr-comment/gr-comment_test.ts
+++ b/polygerrit-ui/app/elements/shared/gr-comment/gr-comment_test.ts
@@ -98,30 +98,30 @@
initiallyCollapsedElement,
/* HTML */ `
<gr-endpoint-decorator name="comment">
- <gr-endpoint-param name="comment"></gr-endpoint-param>
- <gr-endpoint-param name="editing"></gr-endpoint-param>
- <gr-endpoint-param name="message"></gr-endpoint-param>
- <gr-endpoint-param name="isDraft"></gr-endpoint-param>
+ <gr-endpoint-param name="comment"> </gr-endpoint-param>
+ <gr-endpoint-param name="editing"> </gr-endpoint-param>
+ <gr-endpoint-param name="message"> </gr-endpoint-param>
+ <gr-endpoint-param name="isDraft"> </gr-endpoint-param>
<div class="container" id="container">
<div class="header" id="header">
<div class="headerLeft">
- <gr-account-label deselected=""></gr-account-label>
+ <gr-account-label deselected=""> </gr-account-label>
</div>
<div class="headerMiddle">
<span class="collapsedContent">
This is the test comment message.
</span>
</div>
- <span class="patchset-text">Patchset 1</span>
+ <span class="patchset-text"> Patchset 1 </span>
<div class="show-hide" tabindex="0">
<label aria-label="Expand" class="show-hide">
- <input checked="" class="show-hide" type="checkbox" />
- <gr-icon id="icon" icon="expand_more"></gr-icon>
+ <md-checkbox checked="" class="show-hide"> </md-checkbox>
+ <gr-icon icon="expand_more" id="icon"> </gr-icon>
</label>
</div>
</div>
<div class="body">
- <gr-endpoint-slot name="above-actions"></gr-endpoint-slot>
+ <gr-endpoint-slot name="above-actions"> </gr-endpoint-slot>
</div>
</div>
</gr-endpoint-decorator>
@@ -140,31 +140,31 @@
element,
/* HTML */ `
<gr-endpoint-decorator name="comment">
- <gr-endpoint-param name="comment"></gr-endpoint-param>
- <gr-endpoint-param name="editing"></gr-endpoint-param>
- <gr-endpoint-param name="message"></gr-endpoint-param>
- <gr-endpoint-param name="isDraft"></gr-endpoint-param>
+ <gr-endpoint-param name="comment"> </gr-endpoint-param>
+ <gr-endpoint-param name="editing"> </gr-endpoint-param>
+ <gr-endpoint-param name="message"> </gr-endpoint-param>
+ <gr-endpoint-param name="isDraft"> </gr-endpoint-param>
<div class="container" id="container">
<div class="header" id="header">
<div class="headerLeft">
- <gr-account-label deselected=""></gr-account-label>
+ <gr-account-label deselected=""> </gr-account-label>
</div>
<div class="headerMiddle"></div>
- <span class="patchset-text">Patchset 1</span>
- <span class="separator"></span>
+ <span class="patchset-text"> Patchset 1 </span>
+ <span class="separator"> </span>
<span class="date" tabindex="0">
- <gr-date-formatter withtooltip=""></gr-date-formatter>
+ <gr-date-formatter withtooltip=""> </gr-date-formatter>
</span>
<div class="show-hide" tabindex="0">
<label aria-label="Collapse" class="show-hide">
- <input class="show-hide" type="checkbox" />
- <gr-icon id="icon" icon="expand_less"></gr-icon>
+ <md-checkbox class="show-hide"> </md-checkbox>
+ <gr-icon icon="expand_less" id="icon"> </gr-icon>
</label>
</div>
</div>
<div class="body">
- <gr-formatted-text class="message"></gr-formatted-text>
- <gr-endpoint-slot name="above-actions"></gr-endpoint-slot>
+ <gr-formatted-text class="message"> </gr-formatted-text>
+ <gr-endpoint-slot name="above-actions"> </gr-endpoint-slot>
</div>
</div>
</gr-endpoint-decorator>
@@ -206,10 +206,10 @@
element,
/* HTML */ `
<gr-endpoint-decorator name="comment">
- <gr-endpoint-param name="comment"></gr-endpoint-param>
- <gr-endpoint-param name="editing"></gr-endpoint-param>
- <gr-endpoint-param name="message"></gr-endpoint-param>
- <gr-endpoint-param name="isDraft"></gr-endpoint-param>
+ <gr-endpoint-param name="comment"> </gr-endpoint-param>
+ <gr-endpoint-param name="editing"> </gr-endpoint-param>
+ <gr-endpoint-param name="message"> </gr-endpoint-param>
+ <gr-endpoint-param name="isDraft"> </gr-endpoint-param>
<div class="container draft" id="container">
<div class="header" id="header">
<div class="headerLeft">
@@ -219,31 +219,32 @@
max-width="20em"
title="This draft is only visible to you. To publish drafts, click the 'Reply' or 'Start review' button at the top of the change or press the 'a' key."
>
- <gr-icon filled icon="rate_review"></gr-icon>
- <span class="draftLabel">Draft</span>
+ <gr-icon filled="" icon="rate_review"> </gr-icon>
+ <span class="draftLabel"> Draft </span>
</gr-tooltip-content>
</div>
<div class="headerMiddle"></div>
- <span class="patchset-text">Patchset 1</span>
- <span class="separator"></span>
+ <span class="patchset-text"> Patchset 1 </span>
+ <span class="separator"> </span>
<span class="date" tabindex="0">
- <gr-date-formatter withtooltip=""></gr-date-formatter>
+ <gr-date-formatter withtooltip=""> </gr-date-formatter>
</span>
<div class="show-hide" tabindex="0">
<label aria-label="Collapse" class="show-hide">
- <input class="show-hide" type="checkbox" />
- <gr-icon id="icon" icon="expand_less"></gr-icon>
+ <md-checkbox class="show-hide"> </md-checkbox>
+ <gr-icon icon="expand_less" id="icon"> </gr-icon>
</label>
</div>
</div>
<div class="body">
- <gr-formatted-text class="message"></gr-formatted-text>
- <gr-endpoint-slot name="above-actions"></gr-endpoint-slot>
+ <gr-formatted-text class="message"> </gr-formatted-text>
+ <gr-endpoint-slot name="above-actions"> </gr-endpoint-slot>
<div class="actions">
<div class="leftActions">
<div class="action resolve">
<label>
- <input id="resolvedCheckbox" type="checkbox" />
+ <md-checkbox checked="" id="resolvedCheckbox">
+ </md-checkbox>
Resolved
</label>
</div>
@@ -291,10 +292,10 @@
element,
/* HTML */ `
<gr-endpoint-decorator name="comment">
- <gr-endpoint-param name="comment"></gr-endpoint-param>
- <gr-endpoint-param name="editing"></gr-endpoint-param>
- <gr-endpoint-param name="message"></gr-endpoint-param>
- <gr-endpoint-param name="isDraft"></gr-endpoint-param>
+ <gr-endpoint-param name="comment"> </gr-endpoint-param>
+ <gr-endpoint-param name="editing"> </gr-endpoint-param>
+ <gr-endpoint-param name="message"> </gr-endpoint-param>
+ <gr-endpoint-param name="isDraft"> </gr-endpoint-param>
<div class="container draft" id="container">
<div class="header" id="header">
<div class="headerLeft">
@@ -304,8 +305,8 @@
max-width="20em"
title="This draft is only visible to you. To publish drafts, click the 'Reply' or 'Start review' button at the top of the change or press the 'a' key."
>
- <gr-icon filled icon="rate_review"></gr-icon>
- <span class="draftLabel">Draft</span>
+ <gr-icon filled="" icon="rate_review"> </gr-icon>
+ <span class="draftLabel"> Draft </span>
</gr-tooltip-content>
</div>
<div class="headerMiddle"></div>
@@ -320,15 +321,15 @@
<gr-icon filled="" icon="edit" id="icon"> </gr-icon>
Suggest Edit
</gr-button>
- <span class="patchset-text">Patchset 1</span>
- <span class="separator"></span>
+ <span class="patchset-text"> Patchset 1 </span>
+ <span class="separator"> </span>
<span class="date" tabindex="0">
- <gr-date-formatter withtooltip=""></gr-date-formatter>
+ <gr-date-formatter withtooltip=""> </gr-date-formatter>
</span>
<div class="show-hide" tabindex="0">
<label aria-label="Collapse" class="show-hide">
- <input class="show-hide" type="checkbox" />
- <gr-icon id="icon" icon="expand_less"></gr-icon>
+ <md-checkbox class="show-hide"> </md-checkbox>
+ <gr-icon icon="expand_less" id="icon"> </gr-icon>
</label>
</div>
</div>
@@ -342,12 +343,13 @@
rows="4"
>
</gr-suggestion-textarea>
- <gr-endpoint-slot name="above-actions"></gr-endpoint-slot>
+ <gr-endpoint-slot name="above-actions"> </gr-endpoint-slot>
<div class="actions">
<div class="leftActions">
<div class="action resolve">
<label>
- <input id="resolvedCheckbox" type="checkbox" />
+ <md-checkbox checked="" id="resolvedCheckbox">
+ </md-checkbox>
Resolved
</label>
</div>
diff --git a/polygerrit-ui/app/elements/shared/gr-fix-suggestions/gr-fix-suggestions.ts b/polygerrit-ui/app/elements/shared/gr-fix-suggestions/gr-fix-suggestions.ts
index 66aafbb..7c8bcf4 100644
--- a/polygerrit-ui/app/elements/shared/gr-fix-suggestions/gr-fix-suggestions.ts
+++ b/polygerrit-ui/app/elements/shared/gr-fix-suggestions/gr-fix-suggestions.ts
@@ -30,6 +30,8 @@
import {stringToReplacements} from '../../../utils/comment-util';
import {ReportSource} from '../../../services/suggestions/suggestions-service';
import {getFileExtension} from '../../../utils/file-util';
+import '@material/web/checkbox/checkbox';
+import {materialStyles} from '../../../styles/gr-material-styles';
export const COLLAPSE_SUGGESTION_STORAGE_KEY = 'collapseSuggestionStorageKey';
@@ -143,6 +145,7 @@
static override get styles() {
return [
+ materialStyles,
css`
:host {
display: block;
@@ -154,7 +157,7 @@
margin-left: var(--spacing-s);
}
/* just for a11y */
- input.show-hide {
+ md-checkbox.show-hide {
display: none;
}
label.show-hide {
@@ -360,10 +363,9 @@
return html`
<div class="show-hide" tabindex="0">
<label class="show-hide" aria-label=${ariaLabel}>
- <input
- type="checkbox"
+ <md-checkbox
class="show-hide"
- .checked=${this.collapsed}
+ ?checked=${this.collapsed}
@change=${() => {
this.collapsed = !this.collapsed;
if (this.collapsed) {
@@ -382,7 +384,7 @@
);
}
}}
- />
+ ></md-checkbox>
<gr-icon icon=${icon} id="icon"></gr-icon>
</label>
</div>
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface-element.ts b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface-element.ts
index 6d8496d..8417a94 100644
--- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface-element.ts
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface-element.ts
@@ -195,6 +195,23 @@
}
}
+ async handleBeforeCommitMessage(
+ change: ChangeInfo | ParsedChangeInfo,
+ msg: string
+ ): Promise<boolean> {
+ let okay = true;
+ for (const cb of this._getEventCallbacks(
+ EventType.BEFORE_COMMIT_MSG_EDIT
+ )) {
+ try {
+ okay = (await cb(change, msg)) && okay;
+ } catch (err: unknown) {
+ this.reportError(err, EventType.BEFORE_COMMIT_MSG_EDIT);
+ }
+ }
+ return okay;
+ }
+
handleCommitMessage(change: ChangeInfo | ParsedChangeInfo, msg: string) {
for (const cb of this._getEventCallbacks(EventType.COMMIT_MSG_EDIT)) {
try {
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-types.ts b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-types.ts
index a000456..9f40f7c 100644
--- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-types.ts
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-types.ts
@@ -70,6 +70,17 @@
): string;
addElement(key: TargetElement, el: HTMLElement): void;
getAdminMenuLinks(): MenuLink[];
+ /**
+ * This method is called before handling a commit message edit.
+ * It allows plugins to conditionally block edits.
+ * @param change The relevant change.
+ * @param msg The new commit message text.
+ * @return A promise that resolves to true if the action should proceed.
+ */
+ handleBeforeCommitMessage(
+ change: ChangeInfo | ParsedChangeInfo,
+ msg: string
+ ): Promise<boolean>;
handleCommitMessage(change: ChangeInfo | ParsedChangeInfo, msg: string): void;
canSubmitChange(change: ChangeInfo, revision?: RevisionInfo | null): boolean;
getReviewPostRevert(change?: ChangeInfo): ReviewInput;
diff --git a/polygerrit-ui/app/elements/shared/gr-label-info/gr-label-info.ts b/polygerrit-ui/app/elements/shared/gr-label-info/gr-label-info.ts
index 44eda05..28f6d6e 100644
--- a/polygerrit-ui/app/elements/shared/gr-label-info/gr-label-info.ts
+++ b/polygerrit-ui/app/elements/shared/gr-label-info/gr-label-info.ts
@@ -170,7 +170,7 @@
></gr-account-chip>
${noVoteYet
? this.renderVoteAbility(reviewer)
- : html`${this.renderRemoveVote(reviewer)}`}
+ : html`${this.renderRemoveVote(reviewer, approvalInfo)}`}
</div>`;
}
@@ -187,12 +187,16 @@
return html`<span class="no-votes">No votes</span>`;
}
- private renderRemoveVote(reviewer: AccountInfo) {
+ private renderRemoveVote(
+ reviewer: AccountInfo,
+ approvalInfo: ApprovalInfo | undefined
+ ) {
const accountId = reviewer._account_id;
const canDeleteVote = this.canDeleteVote(
reviewer,
this.mutable,
- this.change
+ this.change,
+ approvalInfo
);
if (!accountId || !canDeleteVote) return;
@@ -253,22 +257,30 @@
/**
* A user is able to delete a vote iff the mutable property is true and the
- * reviewer that left the vote exists in the list of removable_reviewers
+ * reviewer that left the vote exists in the list of removable_labels
* received from the backend.
*/
private canDeleteVote(
- reviewer: ApprovalInfo,
+ reviewer: AccountInfo,
mutable: boolean,
- change?: ParsedChangeInfo
+ change?: ParsedChangeInfo,
+ approvalInfo?: ApprovalInfo
) {
- if (!mutable || !change || !change.removable_reviewers) {
+ if (
+ !mutable ||
+ !change ||
+ !approvalInfo ||
+ !approvalInfo.value ||
+ !change.removable_labels
+ ) {
return false;
}
- const removable = change.removable_reviewers;
- if (removable.find(r => r._account_id === reviewer?._account_id)) {
- return true;
+ const removableAccounts =
+ change.removable_labels[this.label]?.[valueString(approvalInfo.value)];
+ if (!removableAccounts) {
+ return false;
}
- return false;
+ return removableAccounts.find(r => r._account_id === reviewer?._account_id);
}
private async onDeleteVote(accountId: AccountId) {
diff --git a/polygerrit-ui/app/elements/shared/gr-label-info/gr-label-info_test.ts b/polygerrit-ui/app/elements/shared/gr-label-info/gr-label-info_test.ts
index 0514b68..14561ea 100644
--- a/polygerrit-ui/app/elements/shared/gr-label-info/gr-label-info_test.ts
+++ b/polygerrit-ui/app/elements/shared/gr-label-info/gr-label-info_test.ts
@@ -90,7 +90,7 @@
await element.updateComplete;
assert.isUndefined(query<GrButton>(element, 'gr-button'));
- element.change!.removable_reviewers = [account];
+ element.change!.removable_labels = {'Code-Review': {'+1': [account]}};
element.mutable = true;
await element.updateComplete;
assert.isDefined(query<GrButton>(element, 'gr-button'));
@@ -100,7 +100,7 @@
const mock = mockPromise();
const deleteResponse = mock.then(() => new Response(null, {status: 200}));
const deleteStub = stubRestApi('deleteVote').returns(deleteResponse);
- element.change!.removable_reviewers = [account];
+ element.change!.removable_labels = {'Code-Review': {'+1': [account]}};
element.change!.labels!['Code-Review'] = {
...label,
recommended: account,
diff --git a/polygerrit-ui/app/models/change/change-model.ts b/polygerrit-ui/app/models/change/change-model.ts
index df09209..a71d225 100644
--- a/polygerrit-ui/app/models/change/change-model.ts
+++ b/polygerrit-ui/app/models/change/change-model.ts
@@ -21,7 +21,14 @@
RevisionPatchSetNum,
} from '../../types/common';
import {ChangeStatus, DefaultBase} from '../../constants/constants';
-import {combineLatest, forkJoin, from, Observable, of} from 'rxjs';
+import {
+ BehaviorSubject,
+ combineLatest,
+ forkJoin,
+ from,
+ Observable,
+ of,
+} from 'rxjs';
import {
catchError,
filter,
@@ -43,12 +50,16 @@
ParsedChangeInfo,
} from '../../types/types';
import {fire, fireAlert, fireTitleChange} from '../../utils/event-util';
-import {RestApiService} from '../../services/gr-rest-api/gr-rest-api';
+import {
+ RestApiService,
+ SubmittabilityInfo,
+} from '../../services/gr-rest-api/gr-rest-api';
import {select} from '../../utils/observable-util';
import {assertIsDefined} from '../../utils/common-util';
import {Model} from '../base/model';
import {UserModel} from '../user/user-model';
import {define} from '../dependency';
+import {FlagsService, KnownExperimentId} from '../../services/flags/flags';
import {
isOwner,
isUploader,
@@ -86,6 +97,12 @@
loadingStatus: LoadingStatus;
change?: ParsedChangeInfo;
/**
+ * Information about submittablity and evaluation of SRs
+ *
+ * Corresponding values in `change` are always kept in sync.
+ */
+ submittabilityInfo?: SubmittabilityInfo;
+ /**
* The list of reviewed files, kept in the model because we want changes made
* in one view to reflect on other views without re-rendering the other views.
* Undefined means it's still loading and empty set means no files reviewed.
@@ -254,6 +271,32 @@
}
/**
+ * Returns new change object with the fields with submittability related fields
+ * updated.
+ *
+ * - if change is undefined return undefined.
+ * - if change number is different than the one in submittability info, no
+ * updates made
+ */
+export function fillFromSubmittabilityInfo(
+ change?: ParsedChangeInfo,
+ submittabilityInfo?: SubmittabilityInfo
+): ParsedChangeInfo | undefined {
+ if (
+ !change ||
+ !submittabilityInfo ||
+ submittabilityInfo.changeNum !== change._number
+ ) {
+ return change;
+ }
+ return {
+ ...change,
+ submittable: submittabilityInfo.submittable,
+ submit_requirements: submittabilityInfo.submitRequirements,
+ };
+}
+
+/**
* Derives the base patchset number from all the data that can potentially
* influence it. Mostly just returns `viewModelBasePatchNum` or PARENT, but has
* some special logic when looking at merge commits.
@@ -308,17 +351,38 @@
export class ChangeModel extends Model<ChangeState> {
private change?: ParsedChangeInfo;
+ private submittabilityInfo?: SubmittabilityInfo;
+
private patchNum?: RevisionPatchSetNum;
private basePatchNum?: BasePatchSetNum;
private latestPatchNum?: PatchSetNumber;
+ private readonly reloadSubmittabilityTrigger$ = new BehaviorSubject<void>(
+ undefined
+ );
+
public readonly change$ = select(
this.state$,
changeState => changeState.change
);
+ public readonly submittabilityInfo$ = select(
+ this.state$,
+ changeState => changeState.submittabilityInfo
+ );
+
+ public readonly submittable$ = select(
+ this.state$,
+ changeState => changeState.submittabilityInfo?.submittable
+ );
+
+ public readonly submitRequirements$ = select(
+ this.state$,
+ changeState => changeState.submittabilityInfo?.submitRequirements
+ );
+
public readonly changeLoadingStatus$ = select(
this.state$,
changeState => changeState.loadingStatus
@@ -468,7 +532,8 @@
private readonly restApiService: RestApiService,
private readonly userModel: UserModel,
private readonly pluginLoader: PluginLoader,
- private readonly reporting: ReportingService
+ private readonly reporting: ReportingService,
+ private readonly flagsService: FlagsService
) {
super(initialState);
this.patchNum$ = select(
@@ -551,6 +616,7 @@
);
this.subscriptions = [
this.loadChange(),
+ this.loadSubmittabilityInfo(),
this.loadMergeable(),
this.loadReviewedFiles(),
this.setOverviewTitle(),
@@ -561,6 +627,9 @@
this.refuseEditForOpenChange(),
this.refuseEditForClosedChange(),
this.change$.subscribe(change => (this.change = change)),
+ this.submittabilityInfo$.subscribe(
+ submittabilityInfo => (this.submittabilityInfo = submittabilityInfo)
+ ),
this.patchNum$.subscribe(patchNum => (this.patchNum = patchNum)),
this.basePatchNum$.subscribe(
basePatchNum => (this.basePatchNum = basePatchNum)
@@ -742,6 +811,48 @@
.subscribe(mergeable => this.updateState({mergeable}));
}
+ public reloadSubmittability() {
+ this.reloadSubmittabilityTrigger$.next();
+ }
+
+ private loadSubmittabilityInfo() {
+ // Use the same trigger as loadChange, to run SR loading in parallel.
+ return combineLatest([
+ this.viewModel.changeNum$,
+ this.reloadSubmittabilityTrigger$,
+ ])
+ .pipe(
+ map(([changeNum, _]) => changeNum),
+ switchMap(changeNum => {
+ if (!changeNum) {
+ // On change reload changeNum is set to undefined to reset change
+ // state. We propagate undefined and reset the state in this case.
+ return of(undefined);
+ }
+ return from(this.restApiService.getSubmittabilityInfo(changeNum));
+ })
+ )
+ .subscribe(submittabilityInfo => {
+ // TODO(b/445644919): Remove once the submit_requirements is never
+ // requested as part of the change detail.
+ if (
+ !this.flagsService.isEnabled(
+ KnownExperimentId.ASYNC_SUBMIT_REQUIREMENTS
+ )
+ ) {
+ return;
+ }
+ const change = fillFromSubmittabilityInfo(
+ this.change,
+ submittabilityInfo
+ );
+ this.updateState({
+ change,
+ submittabilityInfo,
+ });
+ });
+ }
+
private loadChange() {
return this.viewModel.changeNum$
.pipe(
@@ -1006,11 +1117,24 @@
if (this.change && change?._number !== this.change?._number) {
return;
}
+ if (!change) {
+ this.updateState({
+ change: undefined,
+ loadingStatus: LoadingStatus.NOT_LOADED,
+ });
+ return;
+ }
change = updateRevisionsWithCommitShas(change);
+ // TODO(b/445644919): Remove once the submit_requirements is never requested
+ // as part of the change detail.
+ if (
+ this.flagsService.isEnabled(KnownExperimentId.ASYNC_SUBMIT_REQUIREMENTS)
+ ) {
+ change = fillFromSubmittabilityInfo(change, this.submittabilityInfo);
+ }
this.updateState({
change,
- loadingStatus:
- change === undefined ? LoadingStatus.NOT_LOADED : LoadingStatus.LOADED,
+ loadingStatus: LoadingStatus.LOADED,
});
}
}
diff --git a/polygerrit-ui/app/models/change/change-model_test.ts b/polygerrit-ui/app/models/change/change-model_test.ts
index 2fc8f8a..ce3533e 100644
--- a/polygerrit-ui/app/models/change/change-model_test.ts
+++ b/polygerrit-ui/app/models/change/change-model_test.ts
@@ -16,6 +16,7 @@
createMergeable,
createParsedChange,
createRevision,
+ createSubmitRequirementResultInfo,
TEST_NUMERIC_CHANGE_ID,
} from '../../test/test-data-generators';
import {
@@ -60,6 +61,8 @@
import {SinonStub} from 'sinon';
import {pluginLoaderToken} from '../../elements/shared/gr-js-api-interface/gr-plugin-loader';
import {ShowChangeDetail} from '../../elements/shared/gr-js-api-interface/gr-js-api-types';
+import {SubmittabilityInfo} from '../../services/gr-rest-api/gr-rest-api';
+import {FlagsService, KnownExperimentId} from '../../services/flags/flags';
suite('updateRevisionsWithCommitShas() tests', () => {
test('undefined edit', async () => {
@@ -219,11 +222,26 @@
});
});
+class TestFlagService implements FlagsService {
+ public experiments: Set<string> = new Set();
+
+ finalize() {}
+
+ isEnabled(experimentId: string): boolean {
+ return this.experiments.has(experimentId);
+ }
+
+ get enabledExperiments() {
+ return [...this.experiments];
+ }
+}
+
suite('change model tests', () => {
let changeViewModel: ChangeViewModel;
let changeModel: ChangeModel;
let knownChange: ParsedChangeInfo;
let knownChangeNoRevision: ChangeInfo;
+ let testFlagService: TestFlagService;
const testCompleted = new Subject<void>();
async function waitForLoadingStatus(
@@ -237,6 +255,7 @@
}
setup(() => {
+ testFlagService = new TestFlagService();
changeViewModel = testResolver(changeViewModelToken);
changeModel = new ChangeModel(
testResolver(navigationToken),
@@ -244,7 +263,8 @@
getAppContext().restApiService,
testResolver(userModelToken),
testResolver(pluginLoaderToken),
- getAppContext().reportingService
+ getAppContext().reportingService,
+ testFlagService
);
knownChangeNoRevision = {
...createChange(),
@@ -487,6 +507,132 @@
assert.deepEqual(state?.change, updateRevisionsWithCommitShas(knownChange));
});
+ test('load submit requirements (SRs load first)', async () => {
+ testFlagService.experiments.add(
+ KnownExperimentId.ASYNC_SUBMIT_REQUIREMENTS
+ );
+ const promiseDetail = mockPromise<ParsedChangeInfo | undefined>();
+ const stubDetail = stubRestApi('getChangeDetail').callsFake(
+ () => promiseDetail
+ );
+ const promiseSrs = mockPromise<SubmittabilityInfo | undefined>();
+ const stubSrs = stubRestApi('getSubmittabilityInfo').callsFake(
+ () => promiseSrs
+ );
+ testResolver(changeViewModelToken).setState(createChangeViewState());
+ promiseSrs.resolve({
+ changeNum: knownChange._number,
+ submittable: false,
+ submitRequirements: [createSubmitRequirementResultInfo()],
+ });
+ await waitUntilObserved(
+ changeModel.state$,
+ state => state.submittabilityInfo !== undefined,
+ 'SubmitRequirements was never loaded'
+ );
+ promiseDetail.resolve(knownChange);
+ const state = await waitForLoadingStatus(LoadingStatus.LOADED);
+ assert.isTrue(state.submittabilityInfo?.submittable === false);
+ assert.isTrue(state.submittabilityInfo?.submitRequirements.length === 1);
+ assert.isTrue(state.change?.submittable === false);
+ assert.isTrue(state.change?.submit_requirements?.length === 1);
+ assert.equal(stubDetail.callCount, 1);
+ assert.equal(stubSrs.callCount, 1);
+ });
+
+ test('load submit requirements (Detail load first, experiment enabled)', async () => {
+ testFlagService.experiments.add(
+ KnownExperimentId.ASYNC_SUBMIT_REQUIREMENTS
+ );
+ const promiseDetail = mockPromise<ParsedChangeInfo | undefined>();
+ const stubDetail = stubRestApi('getChangeDetail').callsFake(
+ () => promiseDetail
+ );
+ const promiseSrs = mockPromise<SubmittabilityInfo | undefined>();
+ const stubSrs = stubRestApi('getSubmittabilityInfo').callsFake(
+ () => promiseSrs
+ );
+ let state: ChangeState;
+ testResolver(changeViewModelToken).setState(createChangeViewState());
+ promiseDetail.resolve(knownChange);
+ state = await waitForLoadingStatus(LoadingStatus.LOADED);
+ promiseSrs.resolve({
+ changeNum: knownChange._number,
+ submittable: false,
+ submitRequirements: [createSubmitRequirementResultInfo()],
+ });
+ state = await waitUntilObserved(
+ changeModel.state$,
+ state => state.submittabilityInfo !== undefined,
+ 'SubmitRequirements was never loaded'
+ );
+ assert.isTrue(state.submittabilityInfo?.submittable === false);
+ assert.isTrue(state.submittabilityInfo?.submitRequirements.length === 1);
+ assert.isTrue(state.change?.submittable === false);
+ assert.isTrue(state.change?.submit_requirements?.length === 1);
+ assert.equal(stubDetail.callCount, 1);
+ assert.equal(stubSrs.callCount, 1);
+ });
+
+ test('load submit requirements (experiment disabled)', async () => {
+ const promiseDetail = mockPromise<ParsedChangeInfo | undefined>();
+ const stubDetail = stubRestApi('getChangeDetail').callsFake(
+ () => promiseDetail
+ );
+ const promiseSrs = mockPromise<SubmittabilityInfo | undefined>();
+ const stubSrs = stubRestApi('getSubmittabilityInfo').callsFake(
+ () => promiseSrs
+ );
+ testResolver(changeViewModelToken).setState(createChangeViewState());
+ promiseSrs.resolve(undefined);
+ promiseDetail.resolve({
+ ...knownChange,
+ submittable: false,
+ submit_requirements: [createSubmitRequirementResultInfo()],
+ });
+ const state = await waitForLoadingStatus(LoadingStatus.LOADED);
+ // Validate that submit requirements didn't get reset to undefined.
+ assert.isTrue(state.change?.submittable === false);
+ assert.isTrue(state.change?.submit_requirements?.length === 1);
+ assert.equal(stubDetail.callCount, 1);
+ assert.equal(stubSrs.callCount, 1);
+ });
+
+ test('reload submit requirements', async () => {
+ testFlagService.experiments.add(
+ KnownExperimentId.ASYNC_SUBMIT_REQUIREMENTS
+ );
+ // Set initial state
+ const stubDetail = stubRestApi('getChangeDetail').resolves(knownChange);
+ const stubSrs = stubRestApi('getSubmittabilityInfo');
+ stubSrs.resolves({
+ changeNum: knownChange._number,
+ submittable: false,
+ submitRequirements: [createSubmitRequirementResultInfo()],
+ });
+ testResolver(changeViewModelToken).setState(createChangeViewState());
+ await waitUntilObserved(
+ changeModel.state$,
+ state => state.submittabilityInfo !== undefined,
+ 'SubmitRequirements was never loaded'
+ );
+ await waitForLoadingStatus(LoadingStatus.LOADED);
+ stubSrs.resolves({
+ changeNum: knownChange._number,
+ submittable: true,
+ submitRequirements: [createSubmitRequirementResultInfo()],
+ });
+ changeModel.reloadSubmittability();
+ const state = await waitUntilObserved(
+ changeModel.state$,
+ state => state.submittabilityInfo?.submittable === true,
+ 'Submittability never reloaded'
+ );
+ assert.isTrue(state.change?.submittable);
+ assert.equal(stubDetail.callCount, 1);
+ assert.equal(stubSrs.callCount, 2);
+ });
+
test('navigating to another change', async () => {
// setting up a loaded change
let promise = mockPromise<ParsedChangeInfo | undefined>();
diff --git a/polygerrit-ui/app/models/flows/flows-model.ts b/polygerrit-ui/app/models/flows/flows-model.ts
new file mode 100644
index 0000000..460cec6
--- /dev/null
+++ b/polygerrit-ui/app/models/flows/flows-model.ts
@@ -0,0 +1,97 @@
+/**
+ * @license
+ * Copyright 2025 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+import {BehaviorSubject, combineLatest, from, of} from 'rxjs';
+import {catchError, map, switchMap} from 'rxjs/operators';
+import {ChangeModel} from '../change/change-model';
+import {FlowInfo, FlowInput} from '../../api/rest-api';
+import {Model} from '../base/model';
+import {define} from '../dependency';
+
+import {NumericChangeId} from '../../types/common';
+import {getAppContext} from '../../services/app-context';
+import {KnownExperimentId} from '../../services/flags/flags';
+
+export interface FlowsState {
+ flows: FlowInfo[];
+ loading: boolean;
+ errorMessage?: string;
+}
+
+export const flowsModelToken = define<FlowsModel>('flows-model');
+
+export class FlowsModel extends Model<FlowsState> {
+ readonly flows$ = this.state$.pipe(map(s => s.flows));
+
+ readonly loading$ = this.state$.pipe(map(s => s.loading));
+
+ private readonly reload$ = new BehaviorSubject<void>(undefined);
+
+ private changeNum?: NumericChangeId;
+
+ private readonly restApiService = getAppContext().restApiService;
+
+ private flagService = getAppContext().flagsService;
+
+ constructor(private readonly changeModel: ChangeModel) {
+ super({
+ flows: [],
+ loading: true,
+ });
+
+ this.subscriptions.push(
+ this.changeModel.changeNum$.subscribe(changeNum => {
+ this.changeNum = changeNum;
+ })
+ );
+
+ this.subscriptions.push(
+ combineLatest([this.changeModel.changeNum$, this.reload$])
+ .pipe(
+ switchMap(([changeNum]) => {
+ if (
+ !changeNum ||
+ !this.flagService.isEnabled(KnownExperimentId.SHOW_FLOWS_TAB)
+ )
+ return of([]);
+ this.setState({...this.getState(), loading: true});
+ return from(this.restApiService.listFlows(changeNum)).pipe(
+ catchError(err => {
+ this.setState({
+ ...this.getState(),
+ errorMessage: `Failed to load flows: ${err}`,
+ loading: false,
+ });
+ return of([]);
+ })
+ );
+ })
+ )
+ .subscribe(flows => {
+ this.setState({
+ ...this.getState(),
+ flows: flows ?? [],
+ loading: false,
+ });
+ })
+ );
+ }
+
+ reload() {
+ this.reload$.next();
+ }
+
+ async deleteFlow(flowId: string) {
+ if (!this.changeNum) return;
+ await this.restApiService.deleteFlow(this.changeNum, flowId);
+ this.reload();
+ }
+
+ async createFlow(flowInput: FlowInput) {
+ if (!this.changeNum) return;
+ await this.restApiService.createFlow(this.changeNum, flowInput);
+ this.reload();
+ }
+}
diff --git a/polygerrit-ui/app/services/app-context-init.ts b/polygerrit-ui/app/services/app-context-init.ts
index ab2ae52..a0ab3a3 100644
--- a/polygerrit-ui/app/services/app-context-init.ts
+++ b/polygerrit-ui/app/services/app-context-init.ts
@@ -75,6 +75,7 @@
import {Finalizable} from '../types/types';
import {GrSuggestionsService} from './suggestions/suggestions-service_impl';
import {suggestionsServiceToken} from './suggestions/suggestions-service';
+import {FlowsModel, flowsModelToken} from '../models/flows/flows-model';
/**
* The AppContext lazy initializator for all services
*/
@@ -160,7 +161,8 @@
appContext.restApiService,
resolver(userModelToken),
resolver(pluginLoaderToken),
- appContext.reportingService
+ appContext.reportingService,
+ appContext.flagsService
),
],
[
@@ -249,5 +251,6 @@
resolver(changeModelToken)
),
],
+ [flowsModelToken, () => new FlowsModel(resolver(changeModelToken))],
]);
}
diff --git a/polygerrit-ui/app/services/flags/flags.ts b/polygerrit-ui/app/services/flags/flags.ts
index 33fa6f2..2c7993c 100644
--- a/polygerrit-ui/app/services/flags/flags.ts
+++ b/polygerrit-ui/app/services/flags/flags.ts
@@ -24,4 +24,5 @@
ML_SUGGESTED_EDIT_FEEDBACK = 'UiFeature__ml_suggested_edit_feedback',
ML_SUGGESTED_EDIT_EDITABLE_SUGGESTION = 'UiFeature__ml_suggested_edit_editable_suggestion',
SHOW_FLOWS_TAB = 'UiFeature__show_flows_tab',
+ ASYNC_SUBMIT_REQUIREMENTS = 'UiFeature__async_submit_requirements',
}
diff --git a/polygerrit-ui/app/services/gr-rest-api/gr-rest-api-impl.ts b/polygerrit-ui/app/services/gr-rest-api/gr-rest-api-impl.ts
index 0650a50..ada11f9 100644
--- a/polygerrit-ui/app/services/gr-rest-api/gr-rest-api-impl.ts
+++ b/polygerrit-ui/app/services/gr-rest-api/gr-rest-api-impl.ts
@@ -110,7 +110,11 @@
DiffPreferencesInfo,
IgnoreWhitespaceType,
} from '../../types/diff';
-import {GetDiffCommentsOutput, RestApiService} from './gr-rest-api';
+import {
+ GetDiffCommentsOutput,
+ RestApiService,
+ SubmittabilityInfo,
+} from './gr-rest-api';
import {
CommentSide,
createDefaultDiffPrefs,
@@ -136,6 +140,7 @@
FixReplacementInfo,
FlowInfo,
FlowInput,
+ IsFlowsEnabledInfo,
LabelDefinitionInfo,
LabelDefinitionInput,
SubmitRequirementInput,
@@ -151,6 +156,7 @@
SiteBasedCache,
throwingErrorCallback,
} from '../../elements/shared/gr-rest-api-interface/gr-rest-apis/gr-rest-api-helper';
+import {getAppContext} from '../app-context';
const MAX_PROJECT_RESULTS = 25;
@@ -264,6 +270,8 @@
// Used to serialize requests for certain RPCs
readonly _serialScheduler: Scheduler<Response>;
+ private readonly flags = getAppContext().flagsService;
+
constructor(
private readonly authService: AuthService,
private readonly flagService: FlagsService
@@ -1418,15 +1426,17 @@
ListChangesOption.DOWNLOAD_COMMANDS,
ListChangesOption.MESSAGES,
ListChangesOption.REVIEWER_UPDATES,
- ListChangesOption.SUBMITTABLE,
ListChangesOption.WEB_LINKS,
ListChangesOption.SKIP_DIFFSTAT,
- ListChangesOption.SUBMIT_REQUIREMENTS,
ListChangesOption.PARENTS,
];
if (config?.receive?.enable_signed_push) {
options.push(ListChangesOption.PUSH_CERTIFICATES);
}
+ if (!this.flags.isEnabled(KnownExperimentId.ASYNC_SUBMIT_REQUIREMENTS)) {
+ options.push(ListChangesOption.SUBMITTABLE);
+ options.push(ListChangesOption.SUBMIT_REQUIREMENTS);
+ }
return options;
}
@@ -1485,6 +1495,35 @@
);
}
+ async getSubmittabilityInfo(
+ changeNum: NumericChangeId
+ ): Promise<SubmittabilityInfo | undefined> {
+ if (!this.flags.isEnabled(KnownExperimentId.ASYNC_SUBMIT_REQUIREMENTS)) {
+ return undefined;
+ }
+ const optionsHex = listChangesOptionsToHex(
+ ListChangesOption.SUBMITTABLE,
+ ListChangesOption.SUBMIT_REQUIREMENTS
+ );
+ const change = await this.getChange(
+ changeNum,
+ /* errFn=*/ undefined,
+ optionsHex
+ );
+ if (
+ !change ||
+ change.submittable === undefined ||
+ change.submit_requirements === undefined
+ ) {
+ return undefined;
+ }
+ return {
+ changeNum,
+ submittable: change.submittable,
+ submitRequirements: change.submit_requirements,
+ };
+ }
+
async getChangeCommitInfo(
changeNum: NumericChangeId,
patchNum: PatchSetNum
@@ -3502,15 +3541,10 @@
)
) as Promise<ChangeInfo | undefined>;
} else {
- const params: FetchParams = {q: `change:${changeNum}`};
- if (optionsHex) {
- params['O'] = optionsHex;
- }
return this._restApiHelper
.fetchJSON(
{
url: `/changes/?q=change:${changeNum}`,
- params,
errFn,
anonymizedUrl: '/changes/?q=change:*',
},
@@ -3738,6 +3772,18 @@
}) as Promise<FlowInfo[] | undefined>;
}
+ async getIfFlowsIsEnabled(
+ changeNum: NumericChangeId,
+ errFn?: ErrorCallback
+ ): Promise<IsFlowsEnabledInfo | undefined> {
+ const url = await this._changeBaseURL(changeNum);
+ return this._restApiHelper.fetchJSON({
+ url: `${url}/is-flows-enabled`,
+ errFn,
+ anonymizedUrl: `${ANONYMIZED_CHANGE_BASE_URL}/is-flows-enabled`,
+ }) as Promise<IsFlowsEnabledInfo | undefined>;
+ }
+
async createFlow(
changeNum: NumericChangeId,
flow: FlowInput,
diff --git a/polygerrit-ui/app/services/gr-rest-api/gr-rest-api-impl_test.ts b/polygerrit-ui/app/services/gr-rest-api/gr-rest-api-impl_test.ts
index bfeaa8c..4e33381 100644
--- a/polygerrit-ui/app/services/gr-rest-api/gr-rest-api-impl_test.ts
+++ b/polygerrit-ui/app/services/gr-rest-api/gr-rest-api-impl_test.ts
@@ -2035,6 +2035,15 @@
);
});
+ test('getIfFlowsIsEnabled', async () => {
+ await element.getIfFlowsIsEnabled(changeNum);
+ assert.isTrue(fetchJSONStub.calledOnce);
+ assert.equal(
+ fetchJSONStub.lastCall.args[0].url,
+ `/changes/test-project~${changeNum}/is-flows-enabled`
+ );
+ });
+
test('createFlow', async () => {
const flow: FlowInput = {
stage_expressions: [{condition: 'branch:refs/heads/main'}],
diff --git a/polygerrit-ui/app/services/gr-rest-api/gr-rest-api.ts b/polygerrit-ui/app/services/gr-rest-api/gr-rest-api.ts
index b68f7e6..3a37599 100644
--- a/polygerrit-ui/app/services/gr-rest-api/gr-rest-api.ts
+++ b/polygerrit-ui/app/services/gr-rest-api/gr-rest-api.ts
@@ -104,9 +104,11 @@
FixReplacementInfo,
FlowInfo,
FlowInput,
+ IsFlowsEnabledInfo,
LabelDefinitionInfo,
LabelDefinitionInput,
SubmitRequirementInput,
+ SubmitRequirementResultInfo,
} from '../../api/rest-api';
export interface GetDiffCommentsOutput {
@@ -114,6 +116,12 @@
comments: CommentInfo[];
}
+export interface SubmittabilityInfo {
+ changeNum: NumericChangeId;
+ submittable: boolean;
+ submitRequirements: SubmitRequirementResultInfo[];
+}
+
export interface RestApiService extends Finalizable {
getConfig(
noCache?: boolean,
@@ -206,6 +214,13 @@
): Promise<ParsedChangeInfo | undefined>;
/**
+ * Returns information about submittability and Submit Requirements.
+ */
+ getSubmittabilityInfo(
+ changeNum: NumericChangeId
+ ): Promise<SubmittabilityInfo | undefined>;
+
+ /**
* For every revision of the change returns the list of FileInfo for files
* which are modified compared to revision's parent.
*/
@@ -949,6 +964,11 @@
errFn?: ErrorCallback
): Promise<FlowInfo[] | undefined>;
+ getIfFlowsIsEnabled(
+ changeNum: NumericChangeId,
+ errFn?: ErrorCallback
+ ): Promise<IsFlowsEnabledInfo | undefined>;
+
createFlow(
changeNum: NumericChangeId,
flow: FlowInput,
diff --git a/polygerrit-ui/app/styles/gr-material-styles.ts b/polygerrit-ui/app/styles/gr-material-styles.ts
index 0045108..0621432 100644
--- a/polygerrit-ui/app/styles/gr-material-styles.ts
+++ b/polygerrit-ui/app/styles/gr-material-styles.ts
@@ -95,6 +95,7 @@
/* These colours come from paper-checkbox */
md-checkbox {
+ background-color: var(--background-color-primary);
--md-sys-color-primary: var(--checkbox-primary);
--md-sys-color-on-primary: var(--checkbox-on-primary);
--md-sys-color-on-surface: var(--checkbox-on-surface);
diff --git a/polygerrit-ui/app/test/mocks/gr-rest-api_mock.ts b/polygerrit-ui/app/test/mocks/gr-rest-api_mock.ts
index 31c8660..818dbef 100644
--- a/polygerrit-ui/app/test/mocks/gr-rest-api_mock.ts
+++ b/polygerrit-ui/app/test/mocks/gr-rest-api_mock.ts
@@ -3,7 +3,10 @@
* Copyright 2020 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
-import {RestApiService} from '../../services/gr-rest-api/gr-rest-api';
+import {
+ RestApiService,
+ SubmittabilityInfo,
+} from '../../services/gr-rest-api/gr-rest-api';
import {FlowInfo} from '../../api/rest-api';
import {
AccountCapabilityInfo,
@@ -79,7 +82,7 @@
} from '../../constants/constants';
import {ParsedChangeInfo} from '../../types/types';
import {ErrorCallback} from '../../api/rest';
-import {LabelDefinitionInfo} from '../../api/rest-api';
+import {IsFlowsEnabledInfo, LabelDefinitionInfo} from '../../api/rest-api';
export const grRestApiMock: RestApiService = {
addAccountEmail(): Promise<Response> {
@@ -248,6 +251,9 @@
if (changeNum === undefined) return Promise.resolve(undefined);
return Promise.resolve(createChange() as ParsedChangeInfo);
},
+ getSubmittabilityInfo(): Promise<SubmittabilityInfo | undefined> {
+ return Promise.resolve(undefined);
+ },
getChangeEdit(): Promise<EditInfo | undefined> {
return Promise.resolve(undefined);
},
@@ -339,6 +345,9 @@
getFileContent(): Promise<Response | Base64FileContent | undefined> {
return Promise.resolve(new Response());
},
+ getIfFlowsIsEnabled(): Promise<IsFlowsEnabledInfo | undefined> {
+ return Promise.resolve({enabled: true});
+ },
getRepoName(): Promise<RepoName> {
throw new Error('getRepoName() not implemented by RestApiMock.');
},
diff --git a/polygerrit-ui/app/utils/label-util.ts b/polygerrit-ui/app/utils/label-util.ts
index db02f29..ee7aa3a 100644
--- a/polygerrit-ui/app/utils/label-util.ts
+++ b/polygerrit-ui/app/utils/label-util.ts
@@ -351,7 +351,16 @@
* Show only applicable.
*/
export function getRequirements(change?: ParsedChangeInfo | ChangeInfo) {
- return (change?.submit_requirements ?? []).filter(
+ return getApplicableRequirements(change?.submit_requirements);
+}
+
+/**
+ * Get applicable requirements.
+ */
+export function getApplicableRequirements(
+ requirements?: SubmitRequirementResultInfo[]
+) {
+ return (requirements ?? []).filter(
req => req.status !== SubmitRequirementStatus.NOT_APPLICABLE
);
}
diff --git a/polygerrit-ui/screenshots/Chromium/baseline/gr-change-metadata-dark-dark.png b/polygerrit-ui/screenshots/Chromium/baseline/gr-change-metadata-dark-dark.png
index 10423eb..b94a510 100644
--- a/polygerrit-ui/screenshots/Chromium/baseline/gr-change-metadata-dark-dark.png
+++ b/polygerrit-ui/screenshots/Chromium/baseline/gr-change-metadata-dark-dark.png
Binary files differ
diff --git a/polygerrit-ui/screenshots/Chromium/baseline/gr-change-metadata-show-all-dark-dark.png b/polygerrit-ui/screenshots/Chromium/baseline/gr-change-metadata-show-all-dark-dark.png
index 6da3f5a..d09e5cd 100644
--- a/polygerrit-ui/screenshots/Chromium/baseline/gr-change-metadata-show-all-dark-dark.png
+++ b/polygerrit-ui/screenshots/Chromium/baseline/gr-change-metadata-show-all-dark-dark.png
Binary files differ
diff --git a/polygerrit-ui/screenshots/Chromium/baseline/gr-change-metadata-show-all.png b/polygerrit-ui/screenshots/Chromium/baseline/gr-change-metadata-show-all.png
index 173cdb4..fb4c9cf 100644
--- a/polygerrit-ui/screenshots/Chromium/baseline/gr-change-metadata-show-all.png
+++ b/polygerrit-ui/screenshots/Chromium/baseline/gr-change-metadata-show-all.png
Binary files differ
diff --git a/polygerrit-ui/screenshots/Chromium/baseline/gr-change-metadata.png b/polygerrit-ui/screenshots/Chromium/baseline/gr-change-metadata.png
index c2eaf99..7bea9b3 100644
--- a/polygerrit-ui/screenshots/Chromium/baseline/gr-change-metadata.png
+++ b/polygerrit-ui/screenshots/Chromium/baseline/gr-change-metadata.png
Binary files differ
diff --git a/resources/com/google/gerrit/httpd/raw/PolyGerritIndexHtml.soy b/resources/com/google/gerrit/httpd/raw/PolyGerritIndexHtml.soy
index bc6f619..eb84a2c 100644
--- a/resources/com/google/gerrit/httpd/raw/PolyGerritIndexHtml.soy
+++ b/resources/com/google/gerrit/httpd/raw/PolyGerritIndexHtml.soy
@@ -33,6 +33,7 @@
{@param? defaultDashboardHex: ?}
{@param? dashboardQuery: ?}
{@param? userIsAuthenticated: ?}
+ {@param? submitRequirementsHex: ?}
<!DOCTYPE html>{\n}
<html lang="en">{\n}
<meta charset="utf-8">{\n}
@@ -95,6 +96,9 @@
{if $userIsAuthenticated}
<link rel="preload" href="{$canonicalPath}/{$changeRequestsPath}/edit/?download-commands=true" as="fetch" type="application/json" crossorigin="anonymous"/>{\n}
{/if}
+ {if $submitRequirementsHex}
+ <link rel="preload" href="{$canonicalPath}/{$changeRequestsPath}?O={$submitRequirementsHex}" as="fetch" type="application/json" crossorigin="anonymous"/>{\n}
+ {/if}
{/if}
<link rel="preload" href="{$canonicalPath}/{$changeRequestsPath}/comments?enable-context=true&context-padding=3" as="fetch" type="application/json" crossorigin="anonymous"/>{\n}
<link rel="preload" href="{$canonicalPath}/changes/?q=change:{$changeNum}" as="fetch" type="application/json" crossorigin="anonymous"/>{\n}
diff --git a/tools/maven/gerrit-acceptance-framework_pom.xml b/tools/maven/gerrit-acceptance-framework_pom.xml
index 6815993..73305c1 100644
--- a/tools/maven/gerrit-acceptance-framework_pom.xml
+++ b/tools/maven/gerrit-acceptance-framework_pom.xml
@@ -2,7 +2,7 @@
<modelVersion>4.0.0</modelVersion>
<groupId>com.google.gerrit</groupId>
<artifactId>gerrit-acceptance-framework</artifactId>
- <version>3.12.3-SNAPSHOT</version>
+ <version>3.13.0-SNAPSHOT</version>
<packaging>jar</packaging>
<name>Gerrit Code Review - Acceptance Test Framework</name>
<description>Framework for Gerrit's acceptance tests</description>
diff --git a/tools/maven/gerrit-extension-api_pom.xml b/tools/maven/gerrit-extension-api_pom.xml
index 9a9ed45..8a12804 100644
--- a/tools/maven/gerrit-extension-api_pom.xml
+++ b/tools/maven/gerrit-extension-api_pom.xml
@@ -2,7 +2,7 @@
<modelVersion>4.0.0</modelVersion>
<groupId>com.google.gerrit</groupId>
<artifactId>gerrit-extension-api</artifactId>
- <version>3.12.3-SNAPSHOT</version>
+ <version>3.13.0-SNAPSHOT</version>
<packaging>jar</packaging>
<name>Gerrit Code Review - Extension API</name>
<description>API for Gerrit Extensions</description>
diff --git a/tools/maven/gerrit-plugin-api_pom.xml b/tools/maven/gerrit-plugin-api_pom.xml
index 40349e3..0d254b3 100644
--- a/tools/maven/gerrit-plugin-api_pom.xml
+++ b/tools/maven/gerrit-plugin-api_pom.xml
@@ -2,7 +2,7 @@
<modelVersion>4.0.0</modelVersion>
<groupId>com.google.gerrit</groupId>
<artifactId>gerrit-plugin-api</artifactId>
- <version>3.12.3-SNAPSHOT</version>
+ <version>3.13.0-SNAPSHOT</version>
<packaging>jar</packaging>
<name>Gerrit Code Review - Plugin API</name>
<description>API for Gerrit Plugins</description>
diff --git a/tools/maven/gerrit-war_pom.xml b/tools/maven/gerrit-war_pom.xml
index ddc370b..045a594 100644
--- a/tools/maven/gerrit-war_pom.xml
+++ b/tools/maven/gerrit-war_pom.xml
@@ -2,7 +2,7 @@
<modelVersion>4.0.0</modelVersion>
<groupId>com.google.gerrit</groupId>
<artifactId>gerrit-war</artifactId>
- <version>3.12.3-SNAPSHOT</version>
+ <version>3.13.0-SNAPSHOT</version>
<packaging>war</packaging>
<name>Gerrit Code Review - WAR</name>
<description>Gerrit WAR</description>
diff --git a/tools/nongoogle.bzl b/tools/nongoogle.bzl
index 5eae305..11125d7 100644
--- a/tools/nongoogle.bzl
+++ b/tools/nongoogle.bzl
@@ -385,8 +385,8 @@
maven_jar(
name = "h2",
- artifact = "com.h2database:h2:2.3.232",
- sha1 = "4fcc05d966ccdb2812ae8b9a718f69226c0cf4e2",
+ artifact = "com.h2database:h2:2.4.240",
+ sha1 = "686180ad33981ad943fdc0ab381e619b2c2fdfe5",
)
# JGit's transitive dependencies
diff --git a/version.bzl b/version.bzl
index 02ae444..65c00f3 100644
--- a/version.bzl
+++ b/version.bzl
@@ -2,4 +2,4 @@
# Used by :api_install and :api_deploy targets
# when talking to the destination repository.
#
-GERRIT_VERSION = "3.12.3-SNAPSHOT"
+GERRIT_VERSION = "3.13.0-SNAPSHOT"