Merge "Update web link interfact to allow passing of the change key"
diff --git a/Documentation/config-labels.txt b/Documentation/config-labels.txt
index db0308f..3fa84b1 100644
--- a/Documentation/config-labels.txt
+++ b/Documentation/config-labels.txt
@@ -300,6 +300,10 @@
Matches if the diff between two patch sets was of a certain change kind.
+`REWORK` matches all kind of change kinds because any other change kind
+is just a more trivial version of a rework. This means setting
+`changekind:REWORK` is equivalent to setting `is:ANY`.
+
`NO_CHANGE` is more trivial than a trivial rebase, no code change and
a first parent update, hence this change kind is also matched by
`changekind:TRIVIAL_REBASE`, `changekind:NO_CODE_CHANGE` and
diff --git a/Documentation/config-project-config.txt b/Documentation/config-project-config.txt
index ea8f6d2..9fd5b1b 100644
--- a/Documentation/config-project-config.txt
+++ b/Documentation/config-project-config.txt
@@ -142,7 +142,7 @@
[[receive.requireContributorAgreement]]receive.requireContributorAgreement::
+
Controls whether or not a user must complete a contributor agreement before
-they can upload changes. Default is `INHERIT`. If `All-Project` enables this
+they can upload changes. Default is `INHERIT`. If `All-Projects` enables this
option then the dependent project must set it to false if users are not
required to sign a contributor agreement prior to submitting changes for that
specific project. To use that feature the global option in `gerrit.config`
diff --git a/Documentation/config-submit-requirements.txt b/Documentation/config-submit-requirements.txt
index 3e740a4..2686f39 100644
--- a/Documentation/config-submit-requirements.txt
+++ b/Documentation/config-submit-requirements.txt
@@ -259,6 +259,47 @@
canOverrideInChildProjects = true
----
+
+[[test-submit-requirements]]
+== Testing Submit Requirements
+
+The link:rest-api-changes.html#check-submit-requirement[Check Submit Requirement]
+change endpoint can be used to test submit requirements on any change. Users
+are encouraged to test submit requirements before adding them to the project
+to ensure they work as intended.
+
+.Request
+----
+ POST /changes/myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940/check.submit_requirement HTTP/1.0
+ Content-Type: application/json; charset=UTF-8
+
+ {
+ "name": "Code-Review",
+ "submittability_expression": "label:Code-Review=+2"
+ }
+----
+
+.Response
+----
+ HTTP/1.1 200 OK
+ Content-Disposition: attachment
+ Content-Type: application/json; charset=UTF-8
+
+ )]}'
+ {
+ "name": "Code-Review",
+ "status": "SATISFIED",
+ "submittability_expression_result": {
+ "expression": "label:Code-Review=+2",
+ "fulfilled": true,
+ "passingAtoms": [
+ "label:Code-Review=+2"
+ ]
+ },
+ "is_legacy": false
+ }
+----
+
GERRIT
------
Part of link:index.html[Gerrit Code Review]
diff --git a/Documentation/dev-e2e-tests.txt b/Documentation/dev-e2e-tests.txt
index 802b484..8e5463d 100644
--- a/Documentation/dev-e2e-tests.txt
+++ b/Documentation/dev-e2e-tests.txt
@@ -173,6 +173,9 @@
* `-Dcom.google.gerrit.scenarios.ssh_port=29418`
* `-Dcom.google.gerrit.scenarios.http_port=8080`
* `-Dcom.google.gerrit.scenarios.http_scheme=http`
+* `-Dcom.google.gerrit.scenarios.username=admin`
+* `-Dcom.google.gerrit.scenarios.replica_hostname=localhost`
+* `-Dcom.google.gerrit.scenarios.project_prefix=`
Above, the properties can be set with values matching specific deployment topologies under test.
The name of the property corresponds to the uppercase keyword found in the json file. For example,
@@ -194,6 +197,13 @@
That whole replication time depends on the system under test. Therefore, this property here should
be set to a value high enough, so that the test checks for a done replication at the right time.
+==== Context path
+
+The `context_path` property allows test scenarios to send Gerrit REST requests to Gerrit instances
+that use a context path in the URL. Its default is no context path and can be set using another value:
+
+* `-Dcom.google.gerrit.scenarios.context_path=/context`
+
==== Automatic properties
The link:#_input_file[example keywords,role=external,window=_blank] also include `_PROJECT`,
diff --git a/Documentation/pg-plugin-checks-api.txt b/Documentation/pg-plugin-checks-api.txt
index 4e93da1..8786cc4 100644
--- a/Documentation/pg-plugin-checks-api.txt
+++ b/Documentation/pg-plugin-checks-api.txt
@@ -24,8 +24,8 @@
Here are some examples of open source plugins that make use of the Checks API:
* link:https://gerrit.googlesource.com/plugins/checks/+/master/gr-checks/plugin.js[Gerrit Checks Plugin]
-* link:https://chromium.googlesource.com/infra/gerrit-plugins/buildbucket/+/master/src/main/resources/static/buildbucket.js[Chromium Buildbucket Plugin]
-* link:https://chromium.googlesource.com/infra/gerrit-plugins/code-coverage/+/master/src/main/resources/static/chromium-coverage.js[Chromium Coverage Plugin]
+* link:https://chromium.googlesource.com/infra/gerrit-plugins/buildbucket/+/main/web/plugin.ts[Chromium Buildbucket Plugin]
+* link:https://chromium.googlesource.com/infra/gerrit-plugins/code-coverage/+/main/web/plugin.ts[Chromium Coverage Plugin]
[[register]]
== register
diff --git a/Documentation/rest-api-access.txt b/Documentation/rest-api-access.txt
index 45a39d8..991f36c 100644
--- a/Documentation/rest-api-access.txt
+++ b/Documentation/rest-api-access.txt
@@ -388,7 +388,7 @@
|`revision` ||
The revision of the `refs/meta/config` branch from which the access
rights were loaded.
-|`inherits_from` |not set for the `All-Project` project|
+|`inherits_from` |not set for the `All-Projects` project|
The parent project from which permissions are inherited as a
link:rest-api-projects.html#project-info[ProjectInfo] entity.
|`local` ||
diff --git a/e2e-tests/src/test/resources/data/com/google/gerrit/scenarios/AbandonChange.json b/e2e-tests/src/test/resources/data/com/google/gerrit/scenarios/AbandonChange.json
index 665cc4d..9d24b5b 100644
--- a/e2e-tests/src/test/resources/data/com/google/gerrit/scenarios/AbandonChange.json
+++ b/e2e-tests/src/test/resources/data/com/google/gerrit/scenarios/AbandonChange.json
@@ -1,6 +1,6 @@
[
{
- "url": "HTTP_SCHEME://HOSTNAME:HTTP_PORT/a/changes/",
+ "url": "HTTP_SCHEME://HOSTNAME:HTTP_PORTCONTEXT_PATH/a/changes/",
"number": "NUMBER"
}
]
diff --git a/e2e-tests/src/test/resources/data/com/google/gerrit/scenarios/ApproveChange.json b/e2e-tests/src/test/resources/data/com/google/gerrit/scenarios/ApproveChange.json
index 665cc4d..9d24b5b 100644
--- a/e2e-tests/src/test/resources/data/com/google/gerrit/scenarios/ApproveChange.json
+++ b/e2e-tests/src/test/resources/data/com/google/gerrit/scenarios/ApproveChange.json
@@ -1,6 +1,6 @@
[
{
- "url": "HTTP_SCHEME://HOSTNAME:HTTP_PORT/a/changes/",
+ "url": "HTTP_SCHEME://HOSTNAME:HTTP_PORTCONTEXT_PATH/a/changes/",
"number": "NUMBER"
}
]
diff --git a/e2e-tests/src/test/resources/data/com/google/gerrit/scenarios/CheckMasterBranchReplica1.json b/e2e-tests/src/test/resources/data/com/google/gerrit/scenarios/CheckMasterBranchReplica1.json
index 5b892aa..594903a 100644
--- a/e2e-tests/src/test/resources/data/com/google/gerrit/scenarios/CheckMasterBranchReplica1.json
+++ b/e2e-tests/src/test/resources/data/com/google/gerrit/scenarios/CheckMasterBranchReplica1.json
@@ -1,5 +1,5 @@
[
{
- "url": "HTTP_SCHEME://HOSTNAME:HTTP_PORT1/a/projects/PROJECT/branches/master"
+ "url": "HTTP_SCHEME://REPLICA_HOSTNAME:HTTP_PORT1/a/projects/PROJECT/branches/master"
}
]
diff --git a/e2e-tests/src/test/resources/data/com/google/gerrit/scenarios/CheckNewProjectReplica1.json b/e2e-tests/src/test/resources/data/com/google/gerrit/scenarios/CheckNewProjectReplica1.json
index f15ddae..5221d95 100644
--- a/e2e-tests/src/test/resources/data/com/google/gerrit/scenarios/CheckNewProjectReplica1.json
+++ b/e2e-tests/src/test/resources/data/com/google/gerrit/scenarios/CheckNewProjectReplica1.json
@@ -1,6 +1,6 @@
[
{
- "url": "HTTP_SCHEME://HOSTNAME:HTTP_PORT1/_PROJECT",
+ "url": "HTTP_SCHEME://REPLICA_HOSTNAME:HTTP_PORT1/_PROJECT",
"cmd": "clone"
}
]
diff --git a/e2e-tests/src/test/resources/data/com/google/gerrit/scenarios/CheckProjectsCacheFlushEntries.json b/e2e-tests/src/test/resources/data/com/google/gerrit/scenarios/CheckProjectsCacheFlushEntries.json
index 467661b..75e895e 100644
--- a/e2e-tests/src/test/resources/data/com/google/gerrit/scenarios/CheckProjectsCacheFlushEntries.json
+++ b/e2e-tests/src/test/resources/data/com/google/gerrit/scenarios/CheckProjectsCacheFlushEntries.json
@@ -1,6 +1,6 @@
[
{
- "url": "HTTP_SCHEME://HOSTNAME:HTTP_PORT/a/config/server/caches/projects",
+ "url": "HTTP_SCHEME://HOSTNAME:HTTP_PORTCONTEXT_PATH/a/config/server/caches/projects",
"entries": "PROJECTS_ENTRIES"
}
]
diff --git a/e2e-tests/src/test/resources/data/com/google/gerrit/scenarios/CloneUsingBothProtocols.json b/e2e-tests/src/test/resources/data/com/google/gerrit/scenarios/CloneUsingBothProtocols.json
index 30f5f23..195ed09 100644
--- a/e2e-tests/src/test/resources/data/com/google/gerrit/scenarios/CloneUsingBothProtocols.json
+++ b/e2e-tests/src/test/resources/data/com/google/gerrit/scenarios/CloneUsingBothProtocols.json
@@ -1,10 +1,10 @@
[
{
- "url": "ssh://admin@HOSTNAME:SSH_PORT/_PROJECT",
+ "url": "ssh://USERNAME@HOSTNAME:SSH_PORT/_PROJECT",
"cmd": "clone"
},
{
- "url": "HTTP_SCHEME://HOSTNAME:HTTP_PORT/_PROJECT",
+ "url": "HTTP_SCHEME://HOSTNAME:HTTP_PORTCONTEXT_PATH/_PROJECT",
"cmd": "clone"
}
]
diff --git a/e2e-tests/src/test/resources/data/com/google/gerrit/scenarios/CreateBranch.json b/e2e-tests/src/test/resources/data/com/google/gerrit/scenarios/CreateBranch.json
index 5459f11..487cf02 100644
--- a/e2e-tests/src/test/resources/data/com/google/gerrit/scenarios/CreateBranch.json
+++ b/e2e-tests/src/test/resources/data/com/google/gerrit/scenarios/CreateBranch.json
@@ -1,6 +1,6 @@
[
{
- "url": "HTTP_SCHEME://HOSTNAME:HTTP_PORT/a/projects/PROJECT/branches/",
+ "url": "HTTP_SCHEME://HOSTNAME:HTTP_PORTCONTEXT_PATH/a/projects/PROJECT/branches/",
"project": "PROJECT"
}
]
diff --git a/e2e-tests/src/test/resources/data/com/google/gerrit/scenarios/CreateChange.json b/e2e-tests/src/test/resources/data/com/google/gerrit/scenarios/CreateChange.json
index 70e79ca..8b8a163 100644
--- a/e2e-tests/src/test/resources/data/com/google/gerrit/scenarios/CreateChange.json
+++ b/e2e-tests/src/test/resources/data/com/google/gerrit/scenarios/CreateChange.json
@@ -1,6 +1,6 @@
[
{
- "url": "HTTP_SCHEME://HOSTNAME:HTTP_PORT/a/changes/",
+ "url": "HTTP_SCHEME://HOSTNAME:HTTP_PORTCONTEXT_PATH/a/changes/",
"project": "PROJECT"
}
]
diff --git a/e2e-tests/src/test/resources/data/com/google/gerrit/scenarios/CreateProject.json b/e2e-tests/src/test/resources/data/com/google/gerrit/scenarios/CreateProject.json
index c141bb8..756313a 100644
--- a/e2e-tests/src/test/resources/data/com/google/gerrit/scenarios/CreateProject.json
+++ b/e2e-tests/src/test/resources/data/com/google/gerrit/scenarios/CreateProject.json
@@ -1,6 +1,6 @@
[
{
- "url": "HTTP_SCHEME://HOSTNAME:HTTP_PORT/a/projects/PROJECT",
+ "url": "HTTP_SCHEME://HOSTNAME:HTTP_PORTCONTEXT_PATH/a/projects/PROJECT",
"parent": "PARENT"
}
]
diff --git a/e2e-tests/src/test/resources/data/com/google/gerrit/scenarios/DeleteChange.json b/e2e-tests/src/test/resources/data/com/google/gerrit/scenarios/DeleteChange.json
index 665cc4d..9d24b5b 100644
--- a/e2e-tests/src/test/resources/data/com/google/gerrit/scenarios/DeleteChange.json
+++ b/e2e-tests/src/test/resources/data/com/google/gerrit/scenarios/DeleteChange.json
@@ -1,6 +1,6 @@
[
{
- "url": "HTTP_SCHEME://HOSTNAME:HTTP_PORT/a/changes/",
+ "url": "HTTP_SCHEME://HOSTNAME:HTTP_PORTCONTEXT_PATH/a/changes/",
"number": "NUMBER"
}
]
diff --git a/e2e-tests/src/test/resources/data/com/google/gerrit/scenarios/DeleteProject.json b/e2e-tests/src/test/resources/data/com/google/gerrit/scenarios/DeleteProject.json
index 5720f53..8bec9de 100644
--- a/e2e-tests/src/test/resources/data/com/google/gerrit/scenarios/DeleteProject.json
+++ b/e2e-tests/src/test/resources/data/com/google/gerrit/scenarios/DeleteProject.json
@@ -1,5 +1,5 @@
[
{
- "url": "HTTP_SCHEME://HOSTNAME:HTTP_PORT/a/projects/PROJECT/delete-project~delete"
+ "url": "HTTP_SCHEME://HOSTNAME:HTTP_PORTCONTEXT_PATH/a/projects/PROJECT/delete-project~delete"
}
]
diff --git a/e2e-tests/src/test/resources/data/com/google/gerrit/scenarios/FlushProjectsCache.json b/e2e-tests/src/test/resources/data/com/google/gerrit/scenarios/FlushProjectsCache.json
index e30a2cf..c1dc674 100644
--- a/e2e-tests/src/test/resources/data/com/google/gerrit/scenarios/FlushProjectsCache.json
+++ b/e2e-tests/src/test/resources/data/com/google/gerrit/scenarios/FlushProjectsCache.json
@@ -1,5 +1,5 @@
[
{
- "url": "HTTP_SCHEME://HOSTNAME:HTTP_PORT/a/config/server/caches/projects/flush"
+ "url": "HTTP_SCHEME://HOSTNAME:HTTP_PORTCONTEXT_PATH/a/config/server/caches/projects/flush"
}
]
diff --git a/e2e-tests/src/test/resources/data/com/google/gerrit/scenarios/FlushProjectsCacheThenRebuild.json b/e2e-tests/src/test/resources/data/com/google/gerrit/scenarios/FlushProjectsCacheThenRebuild.json
index e30a2cf..c1dc674 100644
--- a/e2e-tests/src/test/resources/data/com/google/gerrit/scenarios/FlushProjectsCacheThenRebuild.json
+++ b/e2e-tests/src/test/resources/data/com/google/gerrit/scenarios/FlushProjectsCacheThenRebuild.json
@@ -1,5 +1,5 @@
[
{
- "url": "HTTP_SCHEME://HOSTNAME:HTTP_PORT/a/config/server/caches/projects/flush"
+ "url": "HTTP_SCHEME://HOSTNAME:HTTP_PORTCONTEXT_PATH/a/config/server/caches/projects/flush"
}
]
diff --git a/e2e-tests/src/test/resources/data/com/google/gerrit/scenarios/GetMasterBranchRevision.json b/e2e-tests/src/test/resources/data/com/google/gerrit/scenarios/GetMasterBranchRevision.json
index 86a3c28..4f6a104 100644
--- a/e2e-tests/src/test/resources/data/com/google/gerrit/scenarios/GetMasterBranchRevision.json
+++ b/e2e-tests/src/test/resources/data/com/google/gerrit/scenarios/GetMasterBranchRevision.json
@@ -1,5 +1,5 @@
[
{
- "url": "HTTP_SCHEME://HOSTNAME:HTTP_PORT/a/projects/PROJECT/branches/master"
+ "url": "HTTP_SCHEME://HOSTNAME:HTTP_PORTCONTEXT_PATH/a/projects/PROJECT/branches/master"
}
]
diff --git a/e2e-tests/src/test/resources/data/com/google/gerrit/scenarios/GetProjectsCacheEntries.json b/e2e-tests/src/test/resources/data/com/google/gerrit/scenarios/GetProjectsCacheEntries.json
index e4e2643..e77d83b 100644
--- a/e2e-tests/src/test/resources/data/com/google/gerrit/scenarios/GetProjectsCacheEntries.json
+++ b/e2e-tests/src/test/resources/data/com/google/gerrit/scenarios/GetProjectsCacheEntries.json
@@ -1,5 +1,5 @@
[
{
- "url": "HTTP_SCHEME://HOSTNAME:HTTP_PORT/a/config/server/caches/projects"
+ "url": "HTTP_SCHEME://HOSTNAME:HTTP_PORTCONTEXT_PATH/a/config/server/caches/projects"
}
]
diff --git a/e2e-tests/src/test/resources/data/com/google/gerrit/scenarios/ListProjects.json b/e2e-tests/src/test/resources/data/com/google/gerrit/scenarios/ListProjects.json
index f6350be..528ef3e 100644
--- a/e2e-tests/src/test/resources/data/com/google/gerrit/scenarios/ListProjects.json
+++ b/e2e-tests/src/test/resources/data/com/google/gerrit/scenarios/ListProjects.json
@@ -1,5 +1,5 @@
[
{
- "url": "HTTP_SCHEME://HOSTNAME:HTTP_PORT/a/projects/"
+ "url": "HTTP_SCHEME://HOSTNAME:HTTP_PORTCONTEXT_PATH/a/projects/"
}
]
diff --git a/e2e-tests/src/test/resources/data/com/google/gerrit/scenarios/ReplayRecordsFromFeeder.json b/e2e-tests/src/test/resources/data/com/google/gerrit/scenarios/ReplayRecordsFromFeeder.json
index 30f5f23..195ed09 100644
--- a/e2e-tests/src/test/resources/data/com/google/gerrit/scenarios/ReplayRecordsFromFeeder.json
+++ b/e2e-tests/src/test/resources/data/com/google/gerrit/scenarios/ReplayRecordsFromFeeder.json
@@ -1,10 +1,10 @@
[
{
- "url": "ssh://admin@HOSTNAME:SSH_PORT/_PROJECT",
+ "url": "ssh://USERNAME@HOSTNAME:SSH_PORT/_PROJECT",
"cmd": "clone"
},
{
- "url": "HTTP_SCHEME://HOSTNAME:HTTP_PORT/_PROJECT",
+ "url": "HTTP_SCHEME://HOSTNAME:HTTP_PORTCONTEXT_PATH/_PROJECT",
"cmd": "clone"
}
]
diff --git a/e2e-tests/src/test/resources/data/com/google/gerrit/scenarios/RestoreChange.json b/e2e-tests/src/test/resources/data/com/google/gerrit/scenarios/RestoreChange.json
index 665cc4d..9d24b5b 100644
--- a/e2e-tests/src/test/resources/data/com/google/gerrit/scenarios/RestoreChange.json
+++ b/e2e-tests/src/test/resources/data/com/google/gerrit/scenarios/RestoreChange.json
@@ -1,6 +1,6 @@
[
{
- "url": "HTTP_SCHEME://HOSTNAME:HTTP_PORT/a/changes/",
+ "url": "HTTP_SCHEME://HOSTNAME:HTTP_PORTCONTEXT_PATH/a/changes/",
"number": "NUMBER"
}
]
diff --git a/e2e-tests/src/test/resources/data/com/google/gerrit/scenarios/SubmitChange.json b/e2e-tests/src/test/resources/data/com/google/gerrit/scenarios/SubmitChange.json
index 301c65b..13d5802 100644
--- a/e2e-tests/src/test/resources/data/com/google/gerrit/scenarios/SubmitChange.json
+++ b/e2e-tests/src/test/resources/data/com/google/gerrit/scenarios/SubmitChange.json
@@ -1,5 +1,5 @@
[
{
- "url": "HTTP_SCHEME://HOSTNAME:HTTP_PORT/a/changes/"
+ "url": "HTTP_SCHEME://HOSTNAME:HTTP_PORTCONTEXT_PATH/a/changes/"
}
]
diff --git a/e2e-tests/src/test/resources/data/com/google/gerrit/scenarios/SubmitChangeInBranch.json b/e2e-tests/src/test/resources/data/com/google/gerrit/scenarios/SubmitChangeInBranch.json
index 301c65b..13d5802 100644
--- a/e2e-tests/src/test/resources/data/com/google/gerrit/scenarios/SubmitChangeInBranch.json
+++ b/e2e-tests/src/test/resources/data/com/google/gerrit/scenarios/SubmitChangeInBranch.json
@@ -1,5 +1,5 @@
[
{
- "url": "HTTP_SCHEME://HOSTNAME:HTTP_PORT/a/changes/"
+ "url": "HTTP_SCHEME://HOSTNAME:HTTP_PORTCONTEXT_PATH/a/changes/"
}
]
diff --git a/e2e-tests/src/test/scala/com/google/gerrit/scenarios/AbandonChange.scala b/e2e-tests/src/test/scala/com/google/gerrit/scenarios/AbandonChange.scala
index d387a3e..d8ebf7f 100644
--- a/e2e-tests/src/test/scala/com/google/gerrit/scenarios/AbandonChange.scala
+++ b/e2e-tests/src/test/scala/com/google/gerrit/scenarios/AbandonChange.scala
@@ -24,7 +24,6 @@
class AbandonChange extends GerritSimulation {
private val data: FeederBuilder = jsonFile(resource).convert(keys).circular
- private val projectName = className
private var numbersCopy: mutable.Queue[Int] = mutable.Queue[Int]()
private var createChange: Option[CreateChange] = Some(new CreateChange(projectName))
diff --git a/e2e-tests/src/test/scala/com/google/gerrit/scenarios/CheckNewProjectReplica1.scala b/e2e-tests/src/test/scala/com/google/gerrit/scenarios/CheckNewProjectReplica1.scala
index ae4fa80..692d576 100644
--- a/e2e-tests/src/test/scala/com/google/gerrit/scenarios/CheckNewProjectReplica1.scala
+++ b/e2e-tests/src/test/scala/com/google/gerrit/scenarios/CheckNewProjectReplica1.scala
@@ -22,7 +22,6 @@
class CheckNewProjectReplica1 extends GitSimulation {
private val data: FeederBuilder = jsonFile(resource).convert(keys).queue
- private val projectName = className
private lazy val replicationDuration = replicationDelay + SecondsPerWeightUnit
@@ -30,7 +29,6 @@
override def replaceOverride(in: String): String = {
var next = replaceProperty("http_port1", 8081, in)
- next = replaceKeyWith("_project", projectName, next)
super.replaceOverride(next)
}
diff --git a/e2e-tests/src/test/scala/com/google/gerrit/scenarios/CloneUsingBothProtocols.scala b/e2e-tests/src/test/scala/com/google/gerrit/scenarios/CloneUsingBothProtocols.scala
index c283861..d1d9c88 100644
--- a/e2e-tests/src/test/scala/com/google/gerrit/scenarios/CloneUsingBothProtocols.scala
+++ b/e2e-tests/src/test/scala/com/google/gerrit/scenarios/CloneUsingBothProtocols.scala
@@ -22,12 +22,8 @@
class CloneUsingBothProtocols extends GitSimulation {
private val data: FeederBuilder = jsonFile(resource).convert(keys).circular
- private val projectName = className
private val duration = 2 * numberOfUsers
- override def replaceOverride(in: String): String = {
- replaceKeyWith("_project", projectName, in)
- }
private val test: ScenarioBuilder = scenario(uniqueName)
.feed(data)
diff --git a/e2e-tests/src/test/scala/com/google/gerrit/scenarios/FlushProjectsCache.scala b/e2e-tests/src/test/scala/com/google/gerrit/scenarios/FlushProjectsCache.scala
index 9fef2cf..a7bda3c 100644
--- a/e2e-tests/src/test/scala/com/google/gerrit/scenarios/FlushProjectsCache.scala
+++ b/e2e-tests/src/test/scala/com/google/gerrit/scenarios/FlushProjectsCache.scala
@@ -22,7 +22,6 @@
class FlushProjectsCache extends CacheFlushSimulation {
private val data: FeederBuilder = jsonFile(resource).convert(keys).queue
- private val projectName = className
override def relativeRuntimeWeight = 2
diff --git a/e2e-tests/src/test/scala/com/google/gerrit/scenarios/GerritSimulation.scala b/e2e-tests/src/test/scala/com/google/gerrit/scenarios/GerritSimulation.scala
index c199dd9..7946f05 100644
--- a/e2e-tests/src/test/scala/com/google/gerrit/scenarios/GerritSimulation.scala
+++ b/e2e-tests/src/test/scala/com/google/gerrit/scenarios/GerritSimulation.scala
@@ -23,6 +23,7 @@
class GerritSimulation extends Simulation {
implicit val conf: GatlingGitConfiguration = GatlingGitConfiguration()
+ private val defaultHostname: String = "localhost"
protected val numberKey: String = "number"
private val packageName = getClass.getPackage.getName
@@ -42,7 +43,7 @@
protected val SecondsPerWeightUnit = 2
val maxExecutionTime: Int = (SecondsPerWeightUnit * relativeRuntimeWeight * powerFactor).toInt
private var cumulativeWaitTime = 0
-
+ protected var projectName: String = className
/**
* How long a scenario step should wait before starting to execute.
* This is also registering that step's resulting wait time, so that time
@@ -73,16 +74,23 @@
replaceProperty("parent", "All-Projects", parent.toString)
case ("project", project) =>
var precedes = replaceKeyWith("_project", className, project.toString)
- precedes = replaceOverride(precedes)
+ precedes = replaceProperty("project", getFullProjectName(projectName), precedes)
replaceProperty("project", precedes)
case ("url", url) =>
var in = replaceOverride(url.toString)
- in = replaceProperty("hostname", "localhost", in)
+ in = replaceProperty("replica_hostname", getProperty("hostname", defaultHostname), in)
+ in = replaceProperty("hostname", defaultHostname, in)
in = replaceProperty("http_port", 8080, in)
in = replaceProperty("http_scheme", "http", in)
+ in = replaceProperty("username", "admin", in)
+ in = replaceProperty("context_path", "", in)
replaceProperty("ssh_port", 29418, in)
}
+ protected def getFullProjectName(projectName: String) {
+ getProperty("project_prefix", "") + projectName
+ }
+
private def replaceProperty(term: String, in: String): String = {
replaceProperty(term, term, in)
}
diff --git a/e2e-tests/src/test/scala/com/google/gerrit/scenarios/GitSimulation.scala b/e2e-tests/src/test/scala/com/google/gerrit/scenarios/GitSimulation.scala
index e2f13a4..5d5f5d5 100644
--- a/e2e-tests/src/test/scala/com/google/gerrit/scenarios/GitSimulation.scala
+++ b/e2e-tests/src/test/scala/com/google/gerrit/scenarios/GitSimulation.scala
@@ -15,6 +15,7 @@
package com.google.gerrit.scenarios
import java.io.{File, IOException}
+import java.net.URLEncoder
import com.github.barbasa.gatling.git.GitRequestSession
import com.github.barbasa.gatling.git.protocol.GitProtocol
@@ -29,6 +30,11 @@
protected val gitRequest = new GitRequestBuilder(GitRequestSession("${cmd}", "${url}"))
protected val gitProtocol: GitProtocol = GitProtocol()
+ override def replaceOverride(in: String): String = {
+ var next = replaceKeyWith("_project", URLEncoder.encode(getFullProjectName(projectName), "UTF-8"), in)
+ super.replaceOverride(next)
+ }
+
after {
Thread.sleep(5000)
val path = conf.tmpBasePath
diff --git a/e2e-tests/src/test/scala/com/google/gerrit/scenarios/ProjectSimulation.scala b/e2e-tests/src/test/scala/com/google/gerrit/scenarios/ProjectSimulation.scala
index d6cb937..7d7bed7 100644
--- a/e2e-tests/src/test/scala/com/google/gerrit/scenarios/ProjectSimulation.scala
+++ b/e2e-tests/src/test/scala/com/google/gerrit/scenarios/ProjectSimulation.scala
@@ -14,10 +14,12 @@
package com.google.gerrit.scenarios
+import java.net.URLEncoder
+
class ProjectSimulation extends GerritSimulation {
- protected var projectName: String = "defaultTestProject"
+ projectName = "defaultTestProject"
override def replaceOverride(in: String): String = {
- replaceProperty("project", projectName, in)
+ replaceProperty("project", URLEncoder.encode(getFullProjectName(projectName), "UTF-8"), in)
}
}
diff --git a/e2e-tests/src/test/scala/com/google/gerrit/scenarios/ReplayRecordsFromFeeder.scala b/e2e-tests/src/test/scala/com/google/gerrit/scenarios/ReplayRecordsFromFeeder.scala
index 0bd9e4a..c049f09 100644
--- a/e2e-tests/src/test/scala/com/google/gerrit/scenarios/ReplayRecordsFromFeeder.scala
+++ b/e2e-tests/src/test/scala/com/google/gerrit/scenarios/ReplayRecordsFromFeeder.scala
@@ -22,13 +22,9 @@
class ReplayRecordsFromFeeder extends GitSimulation {
private val data: FeederBuilder = jsonFile(resource).convert(keys).circular
- private val projectName = className
override def relativeRuntimeWeight = 30
- override def replaceOverride(in: String): String = {
- replaceKeyWith("_project", projectName, in)
- }
private val test: ScenarioBuilder = scenario(uniqueName)
.repeat(10) {
diff --git a/e2e-tests/src/test/scala/com/google/gerrit/scenarios/RestoreChange.scala b/e2e-tests/src/test/scala/com/google/gerrit/scenarios/RestoreChange.scala
index 81096b0..756a239 100644
--- a/e2e-tests/src/test/scala/com/google/gerrit/scenarios/RestoreChange.scala
+++ b/e2e-tests/src/test/scala/com/google/gerrit/scenarios/RestoreChange.scala
@@ -24,7 +24,6 @@
class RestoreChange extends GerritSimulation {
private val data: FeederBuilder = jsonFile(resource).convert(keys).circular
- private val projectName = className
private var numbersCopy: mutable.Queue[Int] = mutable.Queue[Int]()
override def relativeRuntimeWeight = 10
diff --git a/e2e-tests/src/test/scala/com/google/gerrit/scenarios/SubmitChange.scala b/e2e-tests/src/test/scala/com/google/gerrit/scenarios/SubmitChange.scala
index 20be28a..49f0c4b 100644
--- a/e2e-tests/src/test/scala/com/google/gerrit/scenarios/SubmitChange.scala
+++ b/e2e-tests/src/test/scala/com/google/gerrit/scenarios/SubmitChange.scala
@@ -23,7 +23,6 @@
class SubmitChange extends GerritSimulation {
private val data: FeederBuilder = jsonFile(resource).convert(keys).queue
- private val projectName = className
private var createChange = new CreateChange(projectName)
override def relativeRuntimeWeight = 10
diff --git a/e2e-tests/src/test/scala/com/google/gerrit/scenarios/SubmitChangeInBranch.scala b/e2e-tests/src/test/scala/com/google/gerrit/scenarios/SubmitChangeInBranch.scala
index 9e1431b..ca618c3 100644
--- a/e2e-tests/src/test/scala/com/google/gerrit/scenarios/SubmitChangeInBranch.scala
+++ b/e2e-tests/src/test/scala/com/google/gerrit/scenarios/SubmitChangeInBranch.scala
@@ -25,7 +25,6 @@
class SubmitChangeInBranch extends GerritSimulation {
private val data: FeederBuilder = jsonFile(resource).convert(keys).circular
private var changesCopy: mutable.Queue[Int] = mutable.Queue[Int]()
- private val projectName = className
override def relativeRuntimeWeight = 10
diff --git a/java/com/google/gerrit/server/patch/gitfilediff/GitFileDiffCacheImpl.java b/java/com/google/gerrit/server/patch/gitfilediff/GitFileDiffCacheImpl.java
index a735bd2..2b856fb 100644
--- a/java/com/google/gerrit/server/patch/gitfilediff/GitFileDiffCacheImpl.java
+++ b/java/com/google/gerrit/server/patch/gitfilediff/GitFileDiffCacheImpl.java
@@ -43,6 +43,7 @@
import com.google.gerrit.server.logging.TraceContext.TraceTimer;
import com.google.gerrit.server.patch.DiffExecutor;
import com.google.gerrit.server.patch.DiffNotAvailableException;
+import com.google.gerrit.server.util.git.CloseablePool;
import com.google.inject.Inject;
import com.google.inject.Module;
import com.google.inject.Singleton;
@@ -68,7 +69,6 @@
import org.eclipse.jgit.lib.AbbreviatedObjectId;
import org.eclipse.jgit.lib.Config;
import org.eclipse.jgit.lib.ObjectId;
-import org.eclipse.jgit.lib.ObjectReader;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.patch.FileHeader;
import org.eclipse.jgit.util.io.DisabledOutputStream;
@@ -207,8 +207,7 @@
.collect(Collectors.groupingBy(GitFileDiffCacheKey::project));
for (Map.Entry<Project.NameKey, List<GitFileDiffCacheKey>> entry : byProject.entrySet()) {
- try (Repository repo = repoManager.openRepository(entry.getKey());
- ObjectReader reader = repo.newObjectReader()) {
+ try (Repository repo = repoManager.openRepository(entry.getKey())) {
// Grouping keys by diff options because each group of keys will be processed with a
// separate call to JGit using the DiffFormatter object.
@@ -217,7 +216,7 @@
for (Map.Entry<DiffOptions, List<GitFileDiffCacheKey>> group :
optionsGroups.entrySet()) {
- result.putAll(loadAllImpl(repo, reader, group.getKey(), group.getValue()));
+ result.putAll(loadAllImpl(repo, group.getKey(), group.getValue()));
}
}
}
@@ -232,42 +231,46 @@
* @return The git file diffs for all input keys.
*/
private Map<GitFileDiffCacheKey, GitFileDiff> loadAllImpl(
- Repository repo, ObjectReader reader, DiffOptions options, List<GitFileDiffCacheKey> keys)
+ Repository repo, DiffOptions options, List<GitFileDiffCacheKey> keys)
throws IOException, DiffNotAvailableException {
ImmutableMap.Builder<GitFileDiffCacheKey, GitFileDiff> result =
ImmutableMap.builderWithExpectedSize(keys.size());
Map<GitFileDiffCacheKey, String> filePaths =
keys.stream().collect(Collectors.toMap(identity(), GitFileDiffCacheKey::newFilePath));
- DiffFormatter formatter = createDiffFormatter(options, repo, reader);
- ListMultimap<String, DiffEntry> diffEntries =
- loadDiffEntries(formatter, options, filePaths.values());
- for (GitFileDiffCacheKey key : filePaths.keySet()) {
- String newFilePath = filePaths.get(key);
- if (!diffEntries.containsKey(newFilePath)) {
- result.put(
- key,
- GitFileDiff.empty(
- AbbreviatedObjectId.fromObjectId(key.oldTree()),
- AbbreviatedObjectId.fromObjectId(key.newTree()),
- newFilePath));
- continue;
+ try (CloseablePool<DiffFormatter> diffPool =
+ new CloseablePool<>(() -> createDiffFormatter(options, repo))) {
+ ListMultimap<String, DiffEntry> diffEntries;
+ try (CloseablePool<DiffFormatter>.Handle formatter = diffPool.get()) {
+ diffEntries = loadDiffEntries(formatter.get(), options, filePaths.values());
}
- List<DiffEntry> entries = diffEntries.get(newFilePath);
- if (entries.size() == 1) {
- result.put(key, createGitFileDiff(entries.get(0), formatter, key));
- } else {
- // Handle when JGit returns two {Added, Deleted} entries for the same file. This happens,
- // for example, when a file's mode is changed between patchsets (e.g. converting a
- // symlink to a regular file). We combine both diff entries into a single entry with
- // {changeType = Rewrite}.
- List<GitFileDiff> gitDiffs = new ArrayList<>();
- for (DiffEntry entry : diffEntries.get(newFilePath)) {
- gitDiffs.add(createGitFileDiff(entry, formatter, key));
+ for (GitFileDiffCacheKey key : filePaths.keySet()) {
+ String newFilePath = filePaths.get(key);
+ if (!diffEntries.containsKey(newFilePath)) {
+ result.put(
+ key,
+ GitFileDiff.empty(
+ AbbreviatedObjectId.fromObjectId(key.oldTree()),
+ AbbreviatedObjectId.fromObjectId(key.newTree()),
+ newFilePath));
+ continue;
}
- result.put(key, createRewriteEntry(gitDiffs));
+ List<DiffEntry> entries = diffEntries.get(newFilePath);
+ if (entries.size() == 1) {
+ result.put(key, createGitFileDiff(entries.get(0), key, diffPool));
+ } else {
+ // Handle when JGit returns two {Added, Deleted} entries for the same file. This
+ // happens, for example, when a file's mode is changed between patchsets (e.g.
+ // converting a symlink to a regular file). We combine both diff entries into a single
+ // entry with {changeType = Rewrite}.
+ List<GitFileDiff> gitDiffs = new ArrayList<>();
+ for (DiffEntry entry : diffEntries.get(newFilePath)) {
+ gitDiffs.add(createGitFileDiff(entry, key, diffPool));
+ }
+ result.put(key, createRewriteEntry(gitDiffs));
+ }
}
+ return result.build();
}
- return result.build();
}
private static ListMultimap<String, DiffEntry> loadDiffEntries(
@@ -288,10 +291,9 @@
MultimapBuilder.treeKeys().arrayListValues()::build));
}
- private static DiffFormatter createDiffFormatter(
- DiffOptions diffOptions, Repository repo, ObjectReader reader) {
+ private static DiffFormatter createDiffFormatter(DiffOptions diffOptions, Repository repo) {
try (DiffFormatter diffFormatter = new DiffFormatter(DisabledOutputStream.INSTANCE)) {
- diffFormatter.setReader(reader, repo.getConfig());
+ diffFormatter.setRepository(repo);
RawTextComparator cmp = comparatorFor(diffOptions.whitespace());
diffFormatter.setDiffComparator(cmp);
if (diffOptions.renameScore() != -1) {
@@ -334,25 +336,31 @@
* timeout enforcement.
*/
private GitFileDiff createGitFileDiff(
- DiffEntry diffEntry, DiffFormatter formatter, GitFileDiffCacheKey key) throws IOException {
+ DiffEntry diffEntry, GitFileDiffCacheKey key, CloseablePool<DiffFormatter> diffPool)
+ throws IOException {
if (!key.useTimeout()) {
- FileHeader fileHeader = formatter.toFileHeader(diffEntry);
- return GitFileDiff.create(diffEntry, fileHeader);
+ try (CloseablePool<DiffFormatter>.Handle formatter = diffPool.get()) {
+ FileHeader fileHeader = formatter.get().toFileHeader(diffEntry);
+ return GitFileDiff.create(diffEntry, fileHeader);
+ }
}
- Future<FileHeader> fileHeaderFuture =
+ // This submits the DiffFormatter to a different thread. The CloseablePool and our usage of it
+ // ensures that any DiffFormatter instance and the ObjectReader it references internally is
+ // only used by a single thread concurrently. However, ObjectReaders have a reference to
+ // Repository which might not be thread safe (FileRepository is, DfsRepository might not).
+ // This could lead to a race condition.
+ Future<GitFileDiff> fileDiffFuture =
diffExecutor.submit(
() -> {
- synchronized (diffEntry) {
- return formatter.toFileHeader(diffEntry);
+ try (CloseablePool<DiffFormatter>.Handle formatter = diffPool.get()) {
+ return GitFileDiff.create(diffEntry, formatter.get().toFileHeader(diffEntry));
}
});
try {
// We employ the timeout because of a bug in Myers diff in JGit. See
// bugs.chromium.org/p/gerrit/issues/detail?id=487 for more details. The bug may happen
// if the algorithm used in diffs is HISTOGRAM_WITH_FALLBACK_MYERS.
- fileHeaderFuture.get(timeoutMillis, TimeUnit.MILLISECONDS);
- FileHeader fileHeader = formatter.toFileHeader(diffEntry);
- return GitFileDiff.create(diffEntry, fileHeader);
+ return fileDiffFuture.get(timeoutMillis, TimeUnit.MILLISECONDS);
} catch (InterruptedException | TimeoutException e) {
// If timeout happens, create a negative result
metrics.timeouts.increment();
diff --git a/java/com/google/gerrit/server/project/ProjectConfig.java b/java/com/google/gerrit/server/project/ProjectConfig.java
index 123a14c..d816d84 100644
--- a/java/com/google/gerrit/server/project/ProjectConfig.java
+++ b/java/com/google/gerrit/server/project/ProjectConfig.java
@@ -522,12 +522,6 @@
return sort(contributorAgreements.values());
}
- public void remove(ContributorAgreement section) {
- if (section != null) {
- accessSections.remove(section.getName());
- }
- }
-
public void replace(ContributorAgreement section) {
ContributorAgreement.Builder ca = section.toBuilder();
ca.setAutoVerify(resolve(section.getAutoVerify()));
@@ -1179,6 +1173,10 @@
LabelType.DEF_COPY_ALL_SCORES_IF_NO_CHANGE));
Set<Short> copyValues = new HashSet<>();
for (String value : rc.getStringList(LABEL, name, KEY_COPY_VALUE)) {
+ if (value == null) {
+ // value is null if copyValue in project.config is set to an empty string
+ continue;
+ }
try {
short copyValue = Shorts.checkedCast(PermissionRule.parseInt(value));
if (!copyValues.add(copyValue)) {
diff --git a/java/com/google/gerrit/server/query/approval/ChangeKindPredicate.java b/java/com/google/gerrit/server/query/approval/ChangeKindPredicate.java
index 6866983..f519b16 100644
--- a/java/com/google/gerrit/server/query/approval/ChangeKindPredicate.java
+++ b/java/com/google/gerrit/server/query/approval/ChangeKindPredicate.java
@@ -38,6 +38,12 @@
return true;
}
+ // If the configured change kind (changeKind) is REWORK it means that all kind of change kinds
+ // should be matched, since any other change kind is just a more trivial version of a rework.
+ if (changeKind == ChangeKind.REWORK) {
+ return true;
+ }
+
// If the actual change kind (ctx.changeKind()) is NO_CHANGE it is also matched if the
// configured change kind (changeKind) is:
// * TRIVIAL_REBASE: since NO_CHANGE is a special kind of a trivial rebase
diff --git a/java/com/google/gerrit/server/util/git/BUILD b/java/com/google/gerrit/server/util/git/BUILD
index 4f4ba83..bbc6bf0 100644
--- a/java/com/google/gerrit/server/util/git/BUILD
+++ b/java/com/google/gerrit/server/util/git/BUILD
@@ -7,5 +7,6 @@
deps = [
"//java/com/google/gerrit/entities",
"//lib:jgit",
+ "//lib/flogger:api",
],
)
diff --git a/java/com/google/gerrit/server/util/git/CloseablePool.java b/java/com/google/gerrit/server/util/git/CloseablePool.java
new file mode 100644
index 0000000..442bd09
--- /dev/null
+++ b/java/com/google/gerrit/server/util/git/CloseablePool.java
@@ -0,0 +1,116 @@
+// Copyright (C) 2022 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.util.git;
+
+import com.google.common.flogger.FluentLogger;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.function.Supplier;
+
+/**
+ * Pool to manage resources that need to be closed but to whom we might lose the reference to or
+ * where closing resources individually is not always possible.
+ *
+ * <p>This pool can be used when we want to reuse closable resources in a multithreaded context.
+ * Example:
+ *
+ * <pre>{@code
+ * try (CloseablePool<T> pool = new CloseablePool(() -> new T())) {
+ * for (int i = 0; i < 100; i++) {
+ * executor.submit(() -> {
+ * try (CloseablePool<T>.Handle handle = pool.get()) {
+ * // Do work that might potentially take longer than the timeout.
+ * handle.get(); // pooled instance to be used
+ * }
+ * }).get(1000, MILLISECONDS);
+ * }
+ * }
+ * }</pre>
+ */
+public class CloseablePool<T extends AutoCloseable> implements AutoCloseable {
+ private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+
+ private final Supplier<T> tCreator;
+ private List<T> ts;
+
+ /**
+ * Instantiate a new pool. The {@link Supplier} must be capable of creating a new instance on
+ * every call.
+ */
+ public CloseablePool(Supplier<T> tCreator) {
+ this.ts = new ArrayList<>();
+ this.tCreator = tCreator;
+ }
+
+ /**
+ * Get a shared instance or create a new instance. Close the returned handle to return it to the
+ * pool.
+ */
+ public synchronized Handle get() {
+ if (ts.isEmpty()) {
+ return new Handle(tCreator.get());
+ }
+ return new Handle(ts.remove(ts.size() - 1));
+ }
+
+ private synchronized boolean discard(T t) {
+ if (ts != null) {
+ ts.add(t);
+ return true;
+ }
+ return false;
+ }
+
+ @Override
+ public synchronized void close() {
+ for (T t : ts)
+ try {
+ t.close();
+ } catch (Exception e) {
+ logger.atWarning().withCause(e).log(
+ "Failed to close resource %s in CloseablePool %s", t, this);
+ }
+ ts = null;
+ }
+
+ /**
+ * Wrapper around an {@link AutoCloseable}. Will try to return the resource to the pool and close
+ * it in case the pool was already closed.
+ */
+ public class Handle implements AutoCloseable {
+ private final T t;
+
+ private Handle(T t) {
+ this.t = t;
+ }
+
+ /** Returns the managed instance. */
+ public T get() {
+ return t;
+ }
+
+ @Override
+ public void close() {
+ if (!discard(t)) {
+ try {
+ t.close();
+ } catch (Exception e) {
+ logger.atWarning().withCause(e).log(
+ "Failed to close resource %s in CloseablePool %s", this, CloseablePool.this);
+ }
+ }
+ }
+ }
+}
diff --git a/javatests/com/google/gerrit/acceptance/api/change/StickyApprovalsIT.java b/javatests/com/google/gerrit/acceptance/api/change/StickyApprovalsIT.java
index edc1dc4..8dbef88 100644
--- a/javatests/com/google/gerrit/acceptance/api/change/StickyApprovalsIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/change/StickyApprovalsIT.java
@@ -158,6 +158,16 @@
testStickyOnAnyScore();
}
+ @Test
+ public void stickyOnRework() throws Exception {
+ updateCodeReviewLabel(b -> b.setCopyCondition("changekind:REWORK"));
+
+ // changekind:REWORK should match all kind of changes so that approvals are always copied.
+ // This means setting changekind:REWORK is equivalent to setting is:ANY and we can do the same
+ // assertions for both cases.
+ testStickyOnAnyScore();
+ }
+
private void testStickyOnAnyScore() throws Exception {
for (ChangeKind changeKind :
EnumSet.of(REWORK, TRIVIAL_REBASE, NO_CODE_CHANGE, MERGE_FIRST_PARENT_UPDATE, NO_CHANGE)) {
diff --git a/javatests/com/google/gerrit/server/project/ProjectConfigTest.java b/javatests/com/google/gerrit/server/project/ProjectConfigTest.java
index e98b6bf..2b7d7af 100644
--- a/javatests/com/google/gerrit/server/project/ProjectConfigTest.java
+++ b/javatests/com/google/gerrit/server/project/ProjectConfigTest.java
@@ -1057,6 +1057,24 @@
});
}
+ @Test
+ public void readCopyValues_emptyValueIsIgnored() throws Exception {
+ RevCommit rev =
+ tr.commit()
+ .add(
+ "project.config",
+ "[label \"CustomLabel\"]\n"
+ + " copyValue = 1\n"
+ + " copyValue = 2\n"
+ + " copyValue = \n")
+ .create();
+
+ ProjectConfig cfg = read(rev);
+ Map<String, LabelType> labels = cfg.getLabelSections();
+ assertThat(labels.entrySet().iterator().next().getValue().getCopyValues())
+ .containsExactly((short) 1, (short) 2);
+ }
+
private Path writeDefaultAllProjectsConfig(String... lines) throws IOException {
Path dir = sitePaths.etc_dir.resolve(ALL_PROJECTS.get());
Files.createDirectories(dir);
diff --git a/plugins/gitiles b/plugins/gitiles
index b62b109..557cca1 160000
--- a/plugins/gitiles
+++ b/plugins/gitiles
@@ -1 +1 @@
-Subproject commit b62b1098cfc566f5edb9e9a3fed8be20210675f5
+Subproject commit 557cca12c1d39fc27cf8c6ce764c0ee091632b90
diff --git a/plugins/replication b/plugins/replication
index 9d32843..99c9794 160000
--- a/plugins/replication
+++ b/plugins/replication
@@ -1 +1 @@
-Subproject commit 9d32843db89206fbd4c0b28073190afe1bed69dd
+Subproject commit 99c9794a6d5d649bd60e1f0d8f59f79f929f7ba6
diff --git a/polygerrit-ui/app/.eslintrc.js b/polygerrit-ui/app/.eslintrc.js
index 995999e..8eaff5c 100644
--- a/polygerrit-ui/app/.eslintrc.js
+++ b/polygerrit-ui/app/.eslintrc.js
@@ -88,7 +88,11 @@
// https://eslint.org/docs/rules/no-console
'no-console': [
'error',
- {allow: ['warn', 'error', 'info', 'assert', 'group', 'groupEnd']},
+ {
+ allow: [
+ 'warn', 'error', 'info', 'debug', 'assert', 'group', 'groupEnd',
+ ],
+ },
],
// https://eslint.org/docs/rules/no-multiple-empty-lines
'no-multiple-empty-lines': ['error', {max: 1}],
@@ -432,15 +436,17 @@
'lit/attribute-value-entities': 'error',
'lit/binding-positions': 'error',
'lit/no-duplicate-template-bindings': 'error',
+ 'lit/no-invalid-escape-sequences': 'error',
'lit/no-invalid-html': 'error',
'lit/no-legacy-template-syntax': 'error',
- 'lit/no-property-change-update': 'error',
- 'lit/no-invalid-escape-sequences': 'error',
'lit/no-legacy-imports': 'error',
'lit/no-private-properties': 'error',
+ 'lit/no-property-change-update': 'error',
+ 'lit/no-template-bind': 'error',
'lit/no-useless-template-literals': 'error',
'lit/no-value-attribute': 'error',
'lit/prefer-static-styles': 'error',
+ 'lit/quoted-expressions': ['error', 'never'],
},
},
],
diff --git a/polygerrit-ui/app/api/diff.ts b/polygerrit-ui/app/api/diff.ts
index aa50388..08e2e66 100644
--- a/polygerrit-ui/app/api/diff.ts
+++ b/polygerrit-ui/app/api/diff.ts
@@ -253,6 +253,7 @@
* be a temporary setting until the experiment is concluded.
*/
use_lit_components?: boolean;
+ show_sign_col?: boolean;
}
/**
diff --git a/polygerrit-ui/app/constants/constants.ts b/polygerrit-ui/app/constants/constants.ts
index 01ff6ce..8cdd765 100644
--- a/polygerrit-ui/app/constants/constants.ts
+++ b/polygerrit-ui/app/constants/constants.ts
@@ -260,14 +260,19 @@
NONE = 'NONE',
}
-// TODO(TS): Many properties are omitted here, but they are required.
-// Add default values for missing properties.
-export function createDefaultPreferences() {
+export function createDefaultPreferences(): PreferencesInfo {
return {
changes_per_page: 25,
diff_view: DiffViewMode.SIDE_BY_SIDE,
size_bar_in_change_table: true,
- } as PreferencesInfo;
+ my: [],
+ theme: AppTheme.LIGHT,
+ date_format: DateFormat.EURO,
+ time_format: TimeFormat.HHMM_24,
+ change_table: [],
+ email_strategy: EmailStrategy.ATTENTION_SET_ONLY,
+ default_base_for_merges: DefaultBase.AUTO_MERGE,
+ };
}
// These defaults should match the defaults in
diff --git a/polygerrit-ui/app/constants/reporting.ts b/polygerrit-ui/app/constants/reporting.ts
index 70404c6..8818066 100644
--- a/polygerrit-ui/app/constants/reporting.ts
+++ b/polygerrit-ui/app/constants/reporting.ts
@@ -104,6 +104,7 @@
COMMENT_SAVED = 'comment-saved',
DISCARD_COMMENT = 'discard-comment',
COMMENT_DISCARDED = 'comment-discarded',
+ ROBOT_COMMENTS_STATS = 'robot-comments-stats',
CHECKS_TAB_RENDERED = 'checks-tab-rendered',
CHECKS_CHIP_CLICKED = 'checks-chip-clicked',
CHECKS_CHIP_LINK_CLICKED = 'checks-chip-link-clicked',
diff --git a/polygerrit-ui/app/elements/admin/gr-admin-view/gr-admin-view.ts b/polygerrit-ui/app/elements/admin/gr-admin-view/gr-admin-view.ts
index 10ca808..695aa64 100644
--- a/polygerrit-ui/app/elements/admin/gr-admin-view/gr-admin-view.ts
+++ b/polygerrit-ui/app/elements/admin/gr-admin-view/gr-admin-view.ts
@@ -187,7 +187,7 @@
private renderAdminNav(item: NavLink) {
return html`
<li class="sectionTitle ${this.computeSelectedClass(item.view)}">
- <a class="title" href="${this.computeLinkURL(item)}" rel="noopener"
+ <a class="title" href=${this.computeLinkURL(item)} rel="noopener"
>${item.name}</a
>
</li>
@@ -198,8 +198,8 @@
private renderAdminNavChild(child: SubsectionInterface) {
return html`
- <li class="${this.computeSelectedClass(child.view)}">
- <a href="${this.computeLinkURL(child)}" rel="noopener">${child.name}</a>
+ <li class=${this.computeSelectedClass(child.view)}>
+ <a href=${this.computeLinkURL(child)} rel="noopener">${child.name}</a>
</li>
`;
}
@@ -209,7 +209,7 @@
return html`
<!--If a section has a subsection, render that.-->
- <li class="${this.computeSelectedClass(item.subsection.view)}">
+ <li class=${this.computeSelectedClass(item.subsection.view)}>
${this.renderAdminNavSubsectionUrl(item.subsection)}
</li>
<!--Loop through the links in the sub-section.-->
@@ -223,7 +223,7 @@
if (!subsection!.url) return html`${subsection!.name}`;
return html`
- <a class="title" href="${this.computeLinkURL(subsection)}" rel="noopener">
+ <a class="title" href=${this.computeLinkURL(subsection)} rel="noopener">
${subsection!.name}</a
>
`;
@@ -237,7 +237,7 @@
child.detailType
)}"
>
- <a href="${this.computeLinkURL(child)}">${child.name}</a>
+ <a href=${this.computeLinkURL(child)}>${child.name}</a>
</li>
`;
}
@@ -562,32 +562,33 @@
if (this.needsReload()) await this.reload();
}
- needsReload() {
- if (!this.params) return;
+ needsReload(): boolean {
+ if (!this.params) return false;
+ let needsReload = false;
const newRepoName =
this.params.view === GerritView.REPO ? this.params.repo : undefined;
if (newRepoName !== this.repoName) {
this.repoName = newRepoName;
// Reloads the admin menu.
- return true;
+ needsReload = true;
}
const newGroupId =
this.params.view === GerritView.GROUP ? this.params.groupId : undefined;
if (newGroupId !== this.groupId) {
this.groupId = newGroupId;
// Reloads the admin menu.
- return true;
+ needsReload = true;
}
if (
this.breadcrumbParentName &&
(this.params.view !== GerritView.GROUP || !this.params.groupId) &&
(this.params.view !== GerritView.REPO || !this.params.repo)
) {
- return true;
+ needsReload = true;
}
- return false;
+ return needsReload;
}
// private but used in test
diff --git a/polygerrit-ui/app/elements/admin/gr-admin-view/gr-admin-view_test.ts b/polygerrit-ui/app/elements/admin/gr-admin-view/gr-admin-view_test.ts
index 5574ad1..0f473c3 100644
--- a/polygerrit-ui/app/elements/admin/gr-admin-view/gr-admin-view_test.ts
+++ b/polygerrit-ui/app/elements/admin/gr-admin-view/gr-admin-view_test.ts
@@ -256,6 +256,30 @@
assert.equal(reloadStub.callCount, 1);
});
+ test('Nav is reloaded when changing from repo to group', async () => {
+ element.repoName = 'Test Repo' as RepoName;
+ stubRestApi('getAccount').returns(
+ Promise.resolve({
+ name: 'test-user',
+ registered_on: '2015-03-12 18:32:08.000000000' as Timestamp,
+ })
+ );
+ stubRestApi('getAccountCapabilities').returns(
+ Promise.resolve(createAdminCapabilities())
+ );
+ await element.reload();
+ await element.updateComplete;
+
+ sinon.stub(element, 'computeGroupName');
+ const reloadStub = sinon.stub(element, 'reload');
+ const groupId = '1' as GroupId;
+ element.params = {groupId, view: GerritView.GROUP};
+ await element.updateComplete;
+
+ assert.equal(reloadStub.callCount, 1);
+ assert.equal(element.groupId, groupId);
+ });
+
test('Nav is reloaded when group name changes', async () => {
const newName = 'newName' as GroupName;
const reloadCalled = mockPromise();
diff --git a/polygerrit-ui/app/elements/admin/gr-group/gr-group.ts b/polygerrit-ui/app/elements/admin/gr-group/gr-group.ts
index 19e7aa4..c4ef8e6 100644
--- a/polygerrit-ui/app/elements/admin/gr-group/gr-group.ts
+++ b/polygerrit-ui/app/elements/admin/gr-group/gr-group.ts
@@ -136,8 +136,8 @@
override render() {
return html`
<div class="main gr-form-styles read-only">
- <div id="loading" class="${this.computeLoadingClass()}">Loading...</div>
- <div id="loadedContent" class="${this.computeLoadingClass()}">
+ <div id="loading" class=${this.computeLoadingClass()}>Loading...</div>
+ <div id="loadedContent" class=${this.computeLoadingClass()}>
<h1 id="Title" class="heading-1">${this.originalName}</h1>
<h2 id="configurations" class="heading-2">General</h2>
<div id="form">
@@ -287,9 +287,9 @@
<span class="value">
<gr-select
id="visibleToAll"
- .bindValue="${convertToString(
+ .bindValue=${convertToString(
Boolean(this.groupConfig?.options?.visible_to_all)
- )}"
+ )}
@bind-value-changed=${this.handleOptionsBindValueChanged}
>
<select ?disabled=${this.computeGroupDisabled()}>
diff --git a/polygerrit-ui/app/elements/admin/gr-permission/gr-permission.ts b/polygerrit-ui/app/elements/admin/gr-permission/gr-permission.ts
index 4ebdfc0..dd4d9bd 100644
--- a/polygerrit-ui/app/elements/admin/gr-permission/gr-permission.ts
+++ b/polygerrit-ui/app/elements/admin/gr-permission/gr-permission.ts
@@ -16,28 +16,21 @@
*/
import '@polymer/paper-toggle-button/paper-toggle-button';
-import '../../../styles/gr-form-styles';
-import '../../../styles/gr-menu-page-styles';
-import '../../../styles/gr-paper-styles';
-import '../../../styles/shared-styles';
import '../../shared/gr-autocomplete/gr-autocomplete';
import '../../shared/gr-button/gr-button';
import '../gr-rule-editor/gr-rule-editor';
-import {flush} from '@polymer/polymer/lib/legacy/polymer.dom';
-import {PolymerElement} from '@polymer/polymer/polymer-element';
-import {htmlTemplate} from './gr-permission_html';
+import {css, html, LitElement, PropertyValues} from 'lit';
import {
toSortedPermissionsArray,
PermissionArrayItem,
PermissionArray,
+ AccessPermissionId,
} from '../../../utils/access-util';
-import {customElement, property, observe} from '@polymer/decorators';
+import {customElement, property, query, state} from 'lit/decorators';
import {
LabelNameToLabelTypeInfoMap,
LabelTypeInfoValues,
GroupInfo,
- ProjectAccessGroups,
- GroupId,
GitRef,
RepoName,
} from '../../../types/common';
@@ -52,10 +45,13 @@
EditablePermissionRuleInfo,
EditableProjectAccessGroups,
} from '../gr-repo-access/gr-repo-access-interfaces';
-import {PolymerDomRepeatEvent} from '../../../types/types';
import {getAppContext} from '../../../services/app-context';
import {fireEvent} from '../../../utils/event-util';
-import {PolymerDomRepeatCustomEvent} from '../../../types/types';
+import {sharedStyles} from '../../../styles/shared-styles';
+import {paperStyles} from '../../../styles/gr-paper-styles';
+import {formStyles} from '../../../styles/gr-form-styles';
+import {menuPageStyles} from '../../../styles/gr-menu-page-styles';
+import {when} from 'lit/directives/when';
const MAX_AUTOCOMPLETE_RESULTS = 20;
@@ -63,12 +59,6 @@
type GroupsWithRulesMap = {[ruleId: string]: boolean};
-export interface GrPermission {
- $: {
- groupAutocomplete: GrAutocomplete;
- };
-}
-
interface ComputedLabelValue {
value: number;
text: string;
@@ -95,11 +85,7 @@
* @event added-permission-removed
*/
@customElement('gr-permission')
-export class GrPermission extends PolymerElement {
- static get template() {
- return htmlTemplate;
- }
-
+export class GrPermission extends LitElement {
@property({type: String})
repo?: RepoName;
@@ -109,7 +95,7 @@
@property({type: String})
name?: string;
- @property({type: Object, observer: '_sortPermission', notify: true})
+ @property({type: Object})
permission?: PermissionArrayItem<EditablePermissionInfo>;
@property({type: Object})
@@ -118,76 +104,244 @@
@property({type: String})
section?: GitRef;
- @property({type: Boolean, observer: '_handleEditingChanged'})
+ @property({type: Boolean})
editing = false;
- @property({type: Object, computed: '_computeLabel(permission, labels)'})
- _label?: ComputedLabel;
+ @state()
+ private label?: ComputedLabel;
- @property({type: String})
- _groupFilter?: string;
+ @state()
+ private groupFilter?: string;
- @property({type: Object})
- _query: AutocompleteQuery;
+ @state()
+ private query: AutocompleteQuery;
- @property({type: Array})
- _rules?: PermissionArray<EditablePermissionRuleInfo>;
+ @state()
+ rules?: PermissionArray<EditablePermissionRuleInfo | undefined>;
- @property({type: Object})
- _groupsWithRules?: GroupsWithRulesMap;
+ @state()
+ groupsWithRules?: GroupsWithRulesMap;
- @property({type: Boolean})
- _deleted = false;
+ @state()
+ deleted = false;
- @property({type: Boolean})
- _originalExclusiveValue?: boolean;
+ @state()
+ originalExclusiveValue?: boolean;
+
+ @query('#groupAutocomplete')
+ private groupAutocomplete!: GrAutocomplete;
private readonly restApiService = getAppContext().restApiService;
constructor() {
super();
- this._query = () => this._getGroupSuggestions();
- this.addEventListener('access-saved', () => this._handleAccessSaved());
+ this.query = () => this.getGroupSuggestions();
+ this.addEventListener('access-saved', () => this.handleAccessSaved());
}
- override ready() {
- super.ready();
- this._setupValues();
+ override connectedCallback() {
+ super.connectedCallback();
+ this.setupValues();
}
- _setupValues() {
+ override willUpdate(changedProperties: PropertyValues<GrPermission>): void {
+ if (changedProperties.has('editing')) {
+ this.handleEditingChanged(
+ this.editing,
+ changedProperties.get('editing') as boolean
+ );
+ }
+ if (
+ changedProperties.has('permission') ||
+ changedProperties.has('labels')
+ ) {
+ this.label = this.computeLabel();
+ }
+ if (changedProperties.has('permission')) {
+ this.sortPermission(this.permission);
+ }
+ }
+
+ static override styles = [
+ sharedStyles,
+ paperStyles,
+ formStyles,
+ menuPageStyles,
+ css`
+ :host {
+ display: block;
+ margin-bottom: var(--spacing-m);
+ }
+ .header {
+ align-items: baseline;
+ display: flex;
+ justify-content: space-between;
+ margin: var(--spacing-s) var(--spacing-m);
+ }
+ .rules {
+ background: var(--table-header-background-color);
+ border: 1px solid var(--border-color);
+ border-bottom: 0;
+ }
+ .editing .rules {
+ border-bottom: 1px solid var(--border-color);
+ }
+ .title {
+ margin-bottom: var(--spacing-s);
+ }
+ #addRule,
+ #removeBtn {
+ display: none;
+ }
+ .right {
+ display: flex;
+ align-items: center;
+ }
+ .editing #removeBtn {
+ display: block;
+ margin-left: var(--spacing-xl);
+ }
+ .editing #addRule {
+ display: block;
+ padding: var(--spacing-m);
+ }
+ #deletedContainer,
+ .deleted #mainContainer {
+ display: none;
+ }
+ .deleted #deletedContainer {
+ align-items: baseline;
+ border: 1px solid var(--border-color);
+ display: flex;
+ justify-content: space-between;
+ padding: var(--spacing-m);
+ }
+ #mainContainer {
+ display: block;
+ }
+ `,
+ ];
+
+ override render() {
+ if (!this.section || !this.permission) {
+ return;
+ }
+ return html`
+ <section
+ id="permission"
+ class="gr-form-styles ${this.computeSectionClass(
+ this.editing,
+ this.deleted
+ )}"
+ >
+ <div id="mainContainer">
+ <div class="header">
+ <span class="title">${this.name}</span>
+ <div class="right">
+ ${when(
+ !this.permissionIsOwnerOrGlobal(
+ this.permission.id ?? '',
+ this.section
+ ),
+ () => html`
+ <paper-toggle-button
+ id="exclusiveToggle"
+ ?checked=${this.permission?.value.exclusive}
+ ?disabled=${!this.editing}
+ @change=${this.handleValueChange}
+ @click=${this.onTapExclusiveToggle}
+ ></paper-toggle-button
+ >${this.computeExclusiveLabel(this.permission?.value)}
+ `
+ )}
+ <gr-button
+ link=""
+ id="removeBtn"
+ @click=${this.handleRemovePermission}
+ >Remove</gr-button
+ >
+ </div>
+ </div>
+ <!-- end header -->
+ <div class="rules">
+ ${this.rules?.map(
+ (rule, index) => html`
+ <gr-rule-editor
+ .hasRange=${this.computeHasRange(this.name)}
+ .label=${this.label}
+ .editing=${this.editing}
+ .groupId=${rule.id}
+ .groupName=${this.computeGroupName(this.groups, rule.id)}
+ .permission=${this.permission!.id as AccessPermissionId}
+ .rule=${rule}
+ .section=${this.section}
+ @rule-changed=${(e: CustomEvent) =>
+ this.handleRuleChanged(e, index)}
+ @added-rule-removed=${(_: Event) =>
+ this.handleAddedRuleRemoved(index)}
+ ></gr-rule-editor>
+ `
+ )}
+ <div id="addRule">
+ <gr-autocomplete
+ id="groupAutocomplete"
+ .text=${this.groupFilter ?? ''}
+ .query=${this.query}
+ placeholder="Add group"
+ @commit=${this.handleAddRuleItem}
+ >
+ </gr-autocomplete>
+ </div>
+ <!-- end addRule -->
+ </div>
+ <!-- end rules -->
+ </div>
+ <!-- end mainContainer -->
+ <div id="deletedContainer">
+ <span>${this.name} was deleted</span>
+ <gr-button link="" id="undoRemoveBtn" @click=${this.handleUndoRemove}
+ >Undo</gr-button
+ >
+ </div>
+ <!-- end deletedContainer -->
+ </section>
+ `;
+ }
+
+ setupValues() {
if (!this.permission) {
return;
}
- this._originalExclusiveValue = !!this.permission.value.exclusive;
- flush();
+ this.originalExclusiveValue = !!this.permission.value.exclusive;
+ this.requestUpdate();
}
- _handleAccessSaved() {
+ private handleAccessSaved() {
// Set a new 'original' value to keep track of after the value has been
// saved.
- this._setupValues();
+ this.setupValues();
}
- _permissionIsOwnerOrGlobal(permissionId: string, section: string) {
+ private permissionIsOwnerOrGlobal(permissionId: string, section: string) {
return permissionId === 'owner' || section === 'GLOBAL_CAPABILITIES';
}
- _handleEditingChanged(editing: boolean, editingOld: boolean) {
+ private handleEditingChanged(editing: boolean, editingOld: boolean) {
// Ignore when editing gets set initially.
if (!editingOld) {
return;
}
- if (!this.permission || !this._rules) {
+ if (!this.permission || !this.rules) {
return;
}
// Restore original values if no longer editing.
if (!editing) {
- this._deleted = false;
+ this.deleted = false;
delete this.permission.value.deleted;
- this._groupFilter = '';
- this._rules = this._rules.filter(rule => !rule.value.added);
+ this.groupFilter = '';
+ this.rules = this.rules.filter(rule => !rule.value!.added);
+ this.handleRulesChanged();
for (const key of Object.keys(this.permission.value.rules)) {
if (this.permission.value.rules[key].added) {
delete this.permission.value.rules[key];
@@ -195,58 +349,57 @@
}
// Restore exclusive bit to original.
- this.set(
- ['permission', 'value', 'exclusive'],
- this._originalExclusiveValue
- );
+ this.permission.value.exclusive = this.originalExclusiveValue;
+ this.requestUpdate();
}
}
- _handleAddedRuleRemoved(e: PolymerDomRepeatEvent) {
- if (!this._rules) {
+ private handleAddedRuleRemoved(index: number) {
+ if (!this.rules) {
return;
}
- const index = e.model.index;
- this._rules = this._rules
+ this.rules = this.rules
.slice(0, index)
- .concat(this._rules.slice(index + 1, this._rules.length));
+ .concat(this.rules.slice(index + 1, this.rules.length));
+ this.handleRulesChanged();
}
- _handleValueChange() {
+ handleValueChange(e: Event) {
if (!this.permission) {
return;
}
this.permission.value.modified = true;
+ this.permission.value.exclusive = (e.target as HTMLInputElement).checked;
// Allows overall access page to know a change has been made.
fireEvent(this, 'access-modified');
}
- _handleRemovePermission() {
+ handleRemovePermission() {
if (!this.permission) {
return;
}
if (this.permission.value.added) {
fireEvent(this, 'added-permission-removed');
}
- this._deleted = true;
+ this.deleted = true;
this.permission.value.deleted = true;
fireEvent(this, 'access-modified');
}
- @observe('_rules.splices')
- _handleRulesChanged() {
- if (!this._rules) {
+ private handleRulesChanged() {
+ if (!this.rules) {
return;
}
// Update the groups to exclude in the autocomplete.
- this._groupsWithRules = this._computeGroupsWithRules(this._rules);
+ this.groupsWithRules = this.computeGroupsWithRules(this.rules);
}
- _sortPermission(permission: PermissionArrayItem<EditablePermissionInfo>) {
- this._rules = toSortedPermissionsArray(permission.value.rules);
+ sortPermission(permission?: PermissionArrayItem<EditablePermissionInfo>) {
+ this.rules = toSortedPermissionsArray(permission?.value.rules);
+ this.handleRulesChanged();
}
- _computeSectionClass(editing: boolean, deleted: boolean) {
+ computeSectionClass(editing: boolean, deleted: boolean) {
const classList = [];
if (editing) {
classList.push('editing');
@@ -257,18 +410,16 @@
return classList.join(' ');
}
- _handleUndoRemove() {
+ handleUndoRemove() {
if (!this.permission) {
return;
}
- this._deleted = false;
+ this.deleted = false;
delete this.permission.value.deleted;
}
- _computeLabel(
- permission?: PermissionArrayItem<EditablePermissionInfo>,
- labels?: LabelNameToLabelTypeInfoMap
- ): ComputedLabel | undefined {
+ computeLabel(): ComputedLabel | undefined {
+ const {permission, labels} = this;
if (
!labels ||
!permission ||
@@ -287,11 +438,11 @@
}
return {
name: labelName,
- values: this._computeLabelValues(labels[labelName].values),
+ values: this.computeLabelValues(labels[labelName].values),
};
}
- _computeLabelValues(values: LabelTypeInfoValues): ComputedLabelValue[] {
+ computeLabelValues(values: LabelTypeInfoValues): ComputedLabelValue[] {
const valuesArr: ComputedLabelValue[] = [];
const keys = Object.keys(values).sort((a, b) => Number(a) - Number(b));
@@ -307,8 +458,8 @@
return valuesArr;
}
- _computeGroupsWithRules(
- rules: PermissionArray<EditablePermissionRuleInfo>
+ computeGroupsWithRules(
+ rules: PermissionArray<EditablePermissionRuleInfo | undefined>
): GroupsWithRulesMap {
const groups: GroupsWithRulesMap = {};
for (const rule of rules) {
@@ -317,16 +468,19 @@
return groups;
}
- _computeGroupName(groups: ProjectAccessGroups, groupId: GroupId) {
+ computeGroupName(
+ groups: EditableProjectAccessGroups | undefined,
+ groupId: GitRef
+ ) {
return groups && groups[groupId] && groups[groupId].name
? groups[groupId].name
: groupId;
}
- _getGroupSuggestions(): Promise<AutocompleteSuggestion[]> {
+ getGroupSuggestions(): Promise<AutocompleteSuggestion[]> {
return this.restApiService
.getSuggestedGroups(
- this._groupFilter || '',
+ this.groupFilter || '',
this.repo,
MAX_AUTOCOMPLETE_RESULTS
)
@@ -339,7 +493,7 @@
return groups
.filter(
group =>
- this._groupsWithRules && !this._groupsWithRules[group.value.id]
+ this.groupsWithRules && !this.groupsWithRules[group.value.id]
)
.map((group: GroupSuggestion) => {
const autocompleteSuggestion: AutocompleteSuggestion = {
@@ -355,8 +509,8 @@
* Handles adding a skeleton item to the dom-repeat.
* gr-rule-editor handles setting the default values.
*/
- _handleAddRuleItem(e: AutocompleteCommitEvent) {
- if (!this.permission || !this._rules) {
+ async handleAddRuleItem(e: AutocompleteCommitEvent) {
+ if (!this.permission || !this.rules) {
return;
}
@@ -373,33 +527,35 @@
// Purposely don't recompute sorted array so that the newly added rule
// is the last item of the array.
- this.push('_rules', {
- id: groupId,
+ this.rules.push({
+ id: groupId as GitRef,
+ value: undefined,
});
-
- // Add the new group name to the groups object so the name renders
- // correctly.
- if (this.groups && !this.groups[groupId]) {
- this.groups[groupId] = {name: this.$.groupAutocomplete.text};
- }
-
- // Clear the text of the auto-complete box, so that the user can add the
- // next group.
- this.$.groupAutocomplete.text = '';
-
// Wait for new rule to get value populated via gr-rule-editor, and then
// add to permission values as well, so that the change gets propagated
// back to the section. Since the rule is inside a dom-repeat, a flush
// is needed.
- flush();
- const value = this._rules[this._rules.length - 1].value;
- value.added = true;
- // See comment above for why we cannot use "this.set(...)" here.
- this.permission.value.rules[groupId] = value;
+ this.requestUpdate();
+ await this.updateComplete;
+
+ // Add the new group name to the groups object so the name renders
+ // correctly.
+ if (this.groups && !this.groups[groupId]) {
+ this.groups[groupId] = {name: this.groupAutocomplete.text};
+ }
+
+ // Clear the text of the auto-complete box, so that the user can add the
+ // next group.
+ this.groupAutocomplete.text = '';
+
+ const value = this.rules[this.rules.length - 1].value;
+ value!.added = true;
+ this.permission.value.rules[groupId] = value!;
fireEvent(this, 'access-modified');
+ this.requestUpdate();
}
- _computeHasRange(name: string) {
+ computeHasRange(name?: string) {
if (!name) {
return false;
}
@@ -407,28 +563,25 @@
return RANGE_NAMES.includes(name.toUpperCase());
}
- _computeExclusiveLabel(permission?: EditablePermissionInfo) {
+ private computeExclusiveLabel(permission?: EditablePermissionInfo) {
return permission?.exclusive ? 'Exclusive' : 'Not Exclusive';
}
/**
* Work around a issue on iOS when clicking turns into double tap
*/
- _onTapExclusiveToggle(e: Event) {
+ private onTapExclusiveToggle(e: Event) {
e.preventDefault();
}
- _handleRuleChanged(e: PolymerDomRepeatCustomEvent) {
- if (
- this._rules === undefined ||
- (e as CustomEvent).detail.value === undefined
- )
- return;
- const index = Number(e.model.index);
+ private handleRuleChanged(e: CustomEvent, index: number) {
+ if (this.rules === undefined || e.detail.value === undefined) return;
if (isNaN(index)) {
return;
}
- this.splice('_rules', index, (e as CustomEvent).detail.value);
+ this.rules.splice(index, e.detail.value);
+ this.handleRulesChanged();
+ this.requestUpdate();
}
}
diff --git a/polygerrit-ui/app/elements/admin/gr-permission/gr-permission_html.ts b/polygerrit-ui/app/elements/admin/gr-permission/gr-permission_html.ts
deleted file mode 100644
index 779b3fa..0000000
--- a/polygerrit-ui/app/elements/admin/gr-permission/gr-permission_html.ts
+++ /dev/null
@@ -1,147 +0,0 @@
-/**
- * @license
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-import {html} from '@polymer/polymer/lib/utils/html-tag';
-
-export const htmlTemplate = html`
- <style include="shared-styles">
- :host {
- display: block;
- margin-bottom: var(--spacing-m);
- }
- .header {
- align-items: baseline;
- display: flex;
- justify-content: space-between;
- margin: var(--spacing-s) var(--spacing-m);
- }
- .rules {
- background: var(--table-header-background-color);
- border: 1px solid var(--border-color);
- border-bottom: 0;
- }
- .editing .rules {
- border-bottom: 1px solid var(--border-color);
- }
- .title {
- margin-bottom: var(--spacing-s);
- }
- #addRule,
- #removeBtn {
- display: none;
- }
- .right {
- display: flex;
- align-items: center;
- }
- .editing #removeBtn {
- display: block;
- margin-left: var(--spacing-xl);
- }
- .editing #addRule {
- display: block;
- padding: var(--spacing-m);
- }
- #deletedContainer,
- .deleted #mainContainer {
- display: none;
- }
- .deleted #deletedContainer {
- align-items: baseline;
- border: 1px solid var(--border-color);
- display: flex;
- justify-content: space-between;
- padding: var(--spacing-m);
- }
- #mainContainer {
- display: block;
- }
- </style>
- <style include="gr-paper-styles">
- /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
- </style>
- <style include="gr-form-styles">
- /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
- </style>
- <style include="gr-menu-page-styles">
- /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
- </style>
- <section
- id="permission"
- class$="gr-form-styles [[_computeSectionClass(editing, _deleted)]]"
- >
- <div id="mainContainer">
- <div class="header">
- <span class="title">[[name]]</span>
- <div class="right">
- <template
- is="dom-if"
- if="[[!_permissionIsOwnerOrGlobal(permission.id, section)]]"
- >
- <paper-toggle-button
- id="exclusiveToggle"
- checked="{{permission.value.exclusive}}"
- on-change="_handleValueChange"
- disabled$="[[!editing]]"
- on-click="_onTapExclusiveToggle"
- ></paper-toggle-button
- >[[_computeExclusiveLabel(permission.value)]]
- </template>
- <gr-button link="" id="removeBtn" on-click="_handleRemovePermission"
- >Remove</gr-button
- >
- </div>
- </div>
- <!-- end header -->
- <div class="rules">
- <template is="dom-repeat" items="{{_rules}}" as="rule">
- <gr-rule-editor
- has-range="[[_computeHasRange(name)]]"
- label="[[_label]]"
- editing="[[editing]]"
- group-id="[[rule.id]]"
- group-name="[[_computeGroupName(groups, rule.id)]]"
- permission="[[permission.id]]"
- rule="[[rule]]"
- section="[[section]]"
- on-added-rule-removed="_handleAddedRuleRemoved"
- on-rule-changed="_handleRuleChanged"
- ></gr-rule-editor>
- </template>
- <div id="addRule">
- <gr-autocomplete
- id="groupAutocomplete"
- text="{{_groupFilter}}"
- query="[[_query]]"
- placeholder="Add group"
- on-commit="_handleAddRuleItem"
- >
- </gr-autocomplete>
- </div>
- <!-- end addRule -->
- </div>
- <!-- end rules -->
- </div>
- <!-- end mainContainer -->
- <div id="deletedContainer">
- <span>[[name]] was deleted</span>
- <gr-button link="" id="undoRemoveBtn" on-click="_handleUndoRemove"
- >Undo</gr-button
- >
- </div>
- <!-- end deletedContainer -->
- </section>
-`;
diff --git a/polygerrit-ui/app/elements/admin/gr-permission/gr-permission_test.ts b/polygerrit-ui/app/elements/admin/gr-permission/gr-permission_test.ts
index 727b3fe..b77a9ef0 100644
--- a/polygerrit-ui/app/elements/admin/gr-permission/gr-permission_test.ts
+++ b/polygerrit-ui/app/elements/admin/gr-permission/gr-permission_test.ts
@@ -18,7 +18,7 @@
import '../../../test/common-test-setup-karma';
import './gr-permission';
import {GrPermission} from './gr-permission';
-import {stubRestApi} from '../../../test/test-utils';
+import {query, stubRestApi} from '../../../test/test-utils';
import {GitRef, GroupId, GroupName} from '../../../types/common';
import {PermissionAction} from '../../../constants/constants';
import * as MockInteractions from '@polymer/iron-test-helpers/mock-interactions';
@@ -50,7 +50,7 @@
});
suite('unit tests', () => {
- test('_sortPermission', () => {
+ test('sortPermission', async () => {
const permission = {
id: 'submit' as GitRef,
value: {
@@ -78,11 +78,12 @@
},
];
- element._sortPermission(permission);
- assert.deepEqual(element._rules, expectedRules);
+ element.sortPermission(permission);
+ await element.updateComplete;
+ assert.deepEqual(element.rules, expectedRules);
});
- test('_computeLabel and _computeLabelValues', () => {
+ test('computeLabel and computeLabelValues', async () => {
const labels = {
'Code-Review': {
default_value: 0,
@@ -129,15 +130,16 @@
values: expectedLabelValues,
};
+ element.permission = permission;
+ element.labels = labels;
+ await element.updateComplete;
+
assert.deepEqual(
- element._computeLabelValues(labels['Code-Review'].values),
+ element.computeLabelValues(labels['Code-Review'].values),
expectedLabelValues
);
- assert.deepEqual(
- element._computeLabel(permission, labels),
- expectedLabel
- );
+ assert.deepEqual(element.computeLabel(), expectedLabel);
permission = {
id: 'label-reviewDB' as GitRef,
@@ -160,43 +162,46 @@
},
};
- assert.isNotOk(element._computeLabel(permission, labels));
+ element.permission = permission;
+ await element.updateComplete;
+
+ assert.isNotOk(element.computeLabel());
});
- test('_computeSectionClass', () => {
+ test('computeSectionClass', async () => {
let deleted = true;
let editing = false;
- assert.equal(element._computeSectionClass(editing, deleted), 'deleted');
+ assert.equal(element.computeSectionClass(editing, deleted), 'deleted');
deleted = false;
- assert.equal(element._computeSectionClass(editing, deleted), '');
+ assert.equal(element.computeSectionClass(editing, deleted), '');
editing = true;
- assert.equal(element._computeSectionClass(editing, deleted), 'editing');
+ assert.equal(element.computeSectionClass(editing, deleted), 'editing');
deleted = true;
assert.equal(
- element._computeSectionClass(editing, deleted),
+ element.computeSectionClass(editing, deleted),
'editing deleted'
);
});
- test('_computeGroupName', () => {
+ test('computeGroupName', async () => {
const groups = {
abc123: {id: '1' as GroupId, name: 'test group' as GroupName},
bcd234: {id: '1' as GroupId},
};
assert.equal(
- element._computeGroupName(groups, 'abc123' as GroupId),
+ element.computeGroupName(groups, 'abc123' as GitRef),
'test group' as GroupName
);
assert.equal(
- element._computeGroupName(groups, 'bcd234' as GroupId),
+ element.computeGroupName(groups, 'bcd234' as GitRef),
'bcd234' as GroupName
);
});
- test('_computeGroupsWithRules', () => {
+ test('computeGroupsWithRules', async () => {
const rules = [
{
id: '4c97682e6ce6b7247f3381b6f1789356666de7f' as GitRef,
@@ -211,13 +216,14 @@
'4c97682e6ce6b7247f3381b6f1789356666de7f': true,
'global:Project-Owners': true,
};
- assert.deepEqual(element._computeGroupsWithRules(rules), groupsWithRules);
+ assert.deepEqual(element.computeGroupsWithRules(rules), groupsWithRules);
});
- test('_getGroupSuggestions without existing rules', async () => {
- element._groupsWithRules = {};
+ test('getGroupSuggestions without existing rules', async () => {
+ element.groupsWithRules = {};
+ await element.updateComplete;
- const groups = await element._getGroupSuggestions();
+ const groups = await element.getGroupSuggestions();
assert.deepEqual(groups, [
{
name: 'Administrators',
@@ -230,12 +236,13 @@
]);
});
- test('_getGroupSuggestions with existing rules filters them', async () => {
- element._groupsWithRules = {
+ test('getGroupSuggestions with existing rules filters them', async () => {
+ element.groupsWithRules = {
'4c97682e6ce61b7247f3381b6f1789356666de7f': true,
};
+ await element.updateComplete;
- const groups = await element._getGroupSuggestions();
+ const groups = await element.getGroupSuggestions();
assert.deepEqual(groups, [
{
name: 'Anonymous Users',
@@ -244,40 +251,45 @@
]);
});
- test('_handleRemovePermission', () => {
+ test('handleRemovePermission', async () => {
element.editing = true;
element.permission = {id: 'test' as GitRef, value: {rules: {}}};
- element._handleRemovePermission();
- assert.isTrue(element._deleted);
+ element.handleRemovePermission();
+ await element.updateComplete;
+
+ assert.isTrue(element.deleted);
assert.isTrue(element.permission.value.deleted);
element.editing = false;
- assert.isFalse(element._deleted);
+ await element.updateComplete;
+ assert.isFalse(element.deleted);
assert.isNotOk(element.permission.value.deleted);
});
- test('_handleUndoRemove', () => {
+ test('handleUndoRemove', async () => {
element.permission = {
id: 'test' as GitRef,
value: {deleted: true, rules: {}},
};
- element._handleUndoRemove();
- assert.isFalse(element._deleted);
+ element.handleUndoRemove();
+ await element.updateComplete;
+
+ assert.isFalse(element.deleted);
assert.isNotOk(element.permission.value.deleted);
});
- test('_computeHasRange', () => {
- assert.isTrue(element._computeHasRange('Query Limit'));
+ test('computeHasRange', async () => {
+ assert.isTrue(element.computeHasRange('Query Limit'));
- assert.isTrue(element._computeHasRange('Batch Changes Limit'));
+ assert.isTrue(element.computeHasRange('Batch Changes Limit'));
- assert.isFalse(element._computeHasRange('test'));
+ assert.isFalse(element.computeHasRange('test'));
});
});
suite('interactions', () => {
- setup(() => {
- sinon.spy(element, '_computeLabel');
+ setup(async () => {
+ sinon.spy(element, 'computeLabel');
element.name = 'Priority';
element.section = 'refs/*' as GitRef;
element.labels = {
@@ -312,14 +324,17 @@
},
},
};
- element._setupValues();
+ element.setupValues();
+ await element.updateComplete;
flush();
});
- test('adding a rule', () => {
+ test('adding a rule', async () => {
element.name = 'Priority';
element.section = 'refs/*' as GitRef;
element.groups = {};
+ await element.updateComplete;
+
queryAndAssert<GrAutocomplete>(element, '#groupAutocomplete').text =
'ldap/tests te.st';
const e = {
@@ -328,17 +343,16 @@
},
} as CustomEvent<AutocompleteCommitEventDetail>;
element.editing = true;
- assert.equal(element._rules!.length, 2);
- assert.equal(Object.keys(element._groupsWithRules!).length, 2);
- element._handleAddRuleItem(e);
- flush();
+ assert.equal(element.rules!.length, 2);
+ assert.equal(Object.keys(element.groupsWithRules!).length, 2);
+ await element.handleAddRuleItem(e);
assert.deepEqual(element.groups, {
'ldap:CN=test te.st': {
name: 'ldap/tests te.st',
},
});
- assert.equal(element._rules!.length, 3);
- assert.equal(Object.keys(element._groupsWithRules!).length, 3);
+ assert.equal(element.rules!.length, 3);
+ assert.equal(Object.keys(element.groupsWithRules!).length, 3);
assert.deepEqual(element.permission!.value.rules['ldap:CN=test te.st'], {
action: PermissionAction.ALLOW,
min: -2,
@@ -351,7 +365,8 @@
);
// New rule should be removed if cancel from editing.
element.editing = false;
- assert.equal(element._rules!.length, 2);
+ await element.updateComplete;
+ assert.equal(element.rules!.length, 2);
assert.equal(Object.keys(element.permission!.value.rules).length, 2);
});
@@ -359,9 +374,10 @@
element.name = 'Priority';
element.section = 'refs/*' as GitRef;
element.groups = {};
+ await element.updateComplete;
queryAndAssert<GrAutocomplete>(element, '#groupAutocomplete').text =
'new group name';
- assert.equal(element._rules!.length, 2);
+ assert.equal(element.rules!.length, 2);
queryAndAssert<GrRuleEditor>(element, 'gr-rule-editor').dispatchEvent(
new CustomEvent('added-rule-removed', {
composed: true,
@@ -369,24 +385,27 @@
})
);
await flush();
- assert.equal(element._rules!.length, 1);
+ assert.equal(element.rules!.length, 1);
});
- test('removing an added permission', () => {
+ test('removing an added permission', async () => {
const removeStub = sinon.stub();
element.addEventListener('added-permission-removed', removeStub);
element.editing = true;
element.name = 'Priority';
element.section = 'refs/*' as GitRef;
element.permission!.value.added = true;
+ await element.updateComplete;
MockInteractions.tap(queryAndAssert<GrButton>(element, '#removeBtn'));
+ await element.updateComplete;
assert.isTrue(removeStub.called);
});
- test('removing the permission', () => {
+ test('removing the permission', async () => {
element.editing = true;
element.name = 'Priority';
element.section = 'refs/*' as GitRef;
+ await element.updateComplete;
const removeStub = sinon.stub();
element.addEventListener('added-permission-removed', removeStub);
@@ -394,72 +413,72 @@
assert.isFalse(
queryAndAssert(element, '#permission').classList.contains('deleted')
);
- assert.isFalse(element._deleted);
+ assert.isFalse(element.deleted);
MockInteractions.tap(queryAndAssert<GrButton>(element, '#removeBtn'));
+ await element.updateComplete;
assert.isTrue(
queryAndAssert(element, '#permission').classList.contains('deleted')
);
- assert.isTrue(element._deleted);
+ assert.isTrue(element.deleted);
MockInteractions.tap(queryAndAssert<GrButton>(element, '#undoRemoveBtn'));
+ await element.updateComplete;
assert.isFalse(
queryAndAssert(element, '#permission').classList.contains('deleted')
);
- assert.isFalse(element._deleted);
+ assert.isFalse(element.deleted);
assert.isFalse(removeStub.called);
});
- test('modify a permission', () => {
+ test('modify a permission', async () => {
element.editing = true;
element.name = 'Priority';
element.section = 'refs/*' as GitRef;
+ await element.updateComplete;
- assert.isFalse(element._originalExclusiveValue);
+ assert.isFalse(element.originalExclusiveValue);
assert.isNotOk(element.permission!.value.modified);
- queryAndAssert(element, '#exclusiveToggle');
MockInteractions.tap(queryAndAssert(element, '#exclusiveToggle'));
- flush();
+ await element.updateComplete;
assert.isTrue(element.permission!.value.exclusive);
assert.isTrue(element.permission!.value.modified);
- assert.isFalse(element._originalExclusiveValue);
+ assert.isFalse(element.originalExclusiveValue);
element.editing = false;
+ await element.updateComplete;
assert.isFalse(element.permission!.value.exclusive);
});
- test('_handleValueChange', () => {
+ test('modifying emits access-modified event', async () => {
const modifiedHandler = sinon.stub();
+ element.editing = true;
+ element.name = 'Priority';
+ element.section = 'refs/*' as GitRef;
element.permission = {id: '0' as GitRef, value: {rules: {}}};
element.addEventListener('access-modified', modifiedHandler);
+ await element.updateComplete;
assert.isNotOk(element.permission.value.modified);
- element._handleValueChange();
+ MockInteractions.tap(queryAndAssert(element, '#exclusiveToggle'));
+ await element.updateComplete;
assert.isTrue(element.permission.value.modified);
assert.isTrue(modifiedHandler.called);
});
- test('Exclusive hidden for owner permission', () => {
+ test('Exclusive hidden for owner permission', async () => {
queryAndAssert(element, '#exclusiveToggle');
- assert.equal(
- getComputedStyle(queryAndAssert(element, '#exclusiveToggle')).display,
- 'flex'
- );
- element.set(['permission', 'id'], 'owner');
- flush();
- assert.equal(
- getComputedStyle(queryAndAssert(element, '#exclusiveToggle')).display,
- 'none'
- );
+
+ element.permission!.id = 'owner' as GitRef;
+ element.requestUpdate();
+ await element.updateComplete;
+
+ assert.notOk(query(element, '#exclusiveToggle'));
});
- test('Exclusive hidden for any global permissions', () => {
- assert.equal(
- getComputedStyle(queryAndAssert(element, '#exclusiveToggle')).display,
- 'flex'
- );
+ test('Exclusive hidden for any global permissions', async () => {
+ queryAndAssert(element, '#exclusiveToggle');
+
element.section = 'GLOBAL_CAPABILITIES' as GitRef;
- flush();
- assert.equal(
- getComputedStyle(queryAndAssert(element, '#exclusiveToggle')).display,
- 'none'
- );
+ await element.updateComplete;
+
+ assert.notOk(query(element, '#exclusiveToggle'));
});
});
});
diff --git a/polygerrit-ui/app/elements/admin/gr-repo-access/gr-repo-access_test.ts b/polygerrit-ui/app/elements/admin/gr-repo-access/gr-repo-access_test.ts
index 206bed5..37cc488 100644
--- a/polygerrit-ui/app/elements/admin/gr-repo-access/gr-repo-access_test.ts
+++ b/polygerrit-ui/app/elements/admin/gr-repo-access/gr-repo-access_test.ts
@@ -725,7 +725,7 @@
queryAndAssert<GrPermission>(
grAccessSection,
'gr-permission'
- )._handleAddRuleItem({
+ ).handleAddRuleItem({
detail: {value: 'Maintainers'},
} as AutocompleteCommitEvent);
@@ -837,7 +837,7 @@
grAccessSection,
'gr-permission'
)[2];
- newPermission._handleAddRuleItem({
+ newPermission.handleAddRuleItem({
detail: {value: 'Maintainers'},
} as AutocompleteCommitEvent);
await flush();
@@ -969,7 +969,7 @@
queryAndAssert<GrPermission>(
newSection,
'gr-permission'
- )._handleAddRuleItem({
+ ).handleAddRuleItem({
detail: {value: 'Maintainers'},
} as AutocompleteCommitEvent);
await flush();
@@ -1095,7 +1095,7 @@
grAccessSection,
'gr-permission'
)[1];
- readPermission._handleAddRuleItem({
+ readPermission.handleAddRuleItem({
detail: {value: 'Maintainers'},
} as AutocompleteCommitEvent);
await flush();
@@ -1179,7 +1179,7 @@
queryAndAssert<GrPermission>(
newSection,
'gr-permission'
- )._handleAddRuleItem({
+ ).handleAddRuleItem({
detail: {value: 'Maintainers'},
} as AutocompleteCommitEvent);
// Modify a the reference from the default value.
@@ -1259,7 +1259,7 @@
queryAndAssert<GrPermission>(
newSection,
'gr-permission'
- )._handleAddRuleItem({
+ ).handleAddRuleItem({
detail: {value: 'Maintainers'},
} as AutocompleteCommitEvent);
// Modify a the reference from the default value.
diff --git a/polygerrit-ui/app/elements/admin/gr-repo-commands/gr-repo-commands.ts b/polygerrit-ui/app/elements/admin/gr-repo-commands/gr-repo-commands.ts
index 990b34d..f7ad3b8 100644
--- a/polygerrit-ui/app/elements/admin/gr-repo-commands/gr-repo-commands.ts
+++ b/polygerrit-ui/app/elements/admin/gr-repo-commands/gr-repo-commands.ts
@@ -108,10 +108,10 @@
return html`
<div class="main gr-form-styles read-only">
<h1 id="Title" class="heading-1">Repository Commands</h1>
- <div id="loading" class="${this.loading ? 'loading' : ''}">
+ <div id="loading" class=${this.loading ? 'loading' : ''}>
Loading...
</div>
- <div id="loadedContent" class="${this.loading ? 'loading' : ''}">
+ <div id="loadedContent" class=${this.loading ? 'loading' : ''}>
<h2 id="options" class="heading-2">Command</h2>
<div id="form">
<h3 class="heading-3">Create change</h3>
@@ -137,7 +137,7 @@
<gr-endpoint-decorator name="repo-command">
<gr-endpoint-param name="config" .value=${this.repoConfig}>
</gr-endpoint-param>
- <gr-endpoint-param name="repoName" .value="${this.repo}">
+ <gr-endpoint-param name="repoName" .value=${this.repo}>
</gr-endpoint-param>
</gr-endpoint-decorator>
</div>
@@ -159,8 +159,8 @@
<div class="main" slot="main">
<gr-create-change-dialog
id="createNewChangeModal"
- .repoName="${this.repo}"
- .privateByDefault="${this.repoConfig?.private_by_default}"
+ .repoName=${this.repo}
+ .privateByDefault=${this.repoConfig?.private_by_default}
@can-create-change=${() => {
this.handleCanCreateChange();
}}
@@ -178,7 +178,7 @@
return html`
<h3 class="heading-3">${this.repoConfig?.actions['gc']?.label}</h3>
<gr-button
- title="${this.repoConfig?.actions['gc']?.title || ''}"
+ title=${this.repoConfig?.actions['gc']?.title || ''}
?loading=${this.runningGC}
@click=${() => this.handleRunningGC()}
>
diff --git a/polygerrit-ui/app/elements/admin/gr-repo-dashboards/gr-repo-dashboards.ts b/polygerrit-ui/app/elements/admin/gr-repo-dashboards/gr-repo-dashboards.ts
index 3a37971..c2d7615 100644
--- a/polygerrit-ui/app/elements/admin/gr-repo-dashboards/gr-repo-dashboards.ts
+++ b/polygerrit-ui/app/elements/admin/gr-repo-dashboards/gr-repo-dashboards.ts
@@ -90,7 +90,7 @@
info => html`
<tr class="table">
<td class="name">
- <a href="${this._getUrl(info.project, info.id)}"
+ <a href=${this._getUrl(info.project, info.id)}
>${info.path}</a
>
</td>
diff --git a/polygerrit-ui/app/elements/admin/gr-repo-list/gr-repo-list.ts b/polygerrit-ui/app/elements/admin/gr-repo-list/gr-repo-list.ts
index 48983d7..07e89a4 100644
--- a/polygerrit-ui/app/elements/admin/gr-repo-list/gr-repo-list.ts
+++ b/polygerrit-ui/app/elements/admin/gr-repo-list/gr-repo-list.ts
@@ -133,7 +133,7 @@
<td>Loading...</td>
</tr>
</tbody>
- <tbody class="${this.computeLoadingClass(this.loading)}">
+ <tbody class=${this.computeLoadingClass(this.loading)}>
${this.renderRepoList()}
</tbody>
</table>
@@ -168,11 +168,11 @@
return html`
<tr class="table">
<td class="name">
- <a href="${this.computeRepoUrl(item.name)}">${item.name}</a>
+ <a href=${this.computeRepoUrl(item.name)}>${item.name}</a>
</td>
<td class="repositoryBrowser">${this.renderWebLinks(item)}</td>
<td class="changesLink">
- <a href="${this.computeChangesLink(item.name)}">view all</a>
+ <a href=${this.computeChangesLink(item.name)}>view all</a>
</td>
<td class="readOnly">
${item.state === ProjectState.READ_ONLY ? 'Y' : ''}
@@ -189,7 +189,7 @@
private renderWebLink(link: WebLinkInfo) {
return html`
- <a href="${link.url}" class="webLink" rel="noopener" target="_blank">
+ <a href=${link.url} class="webLink" rel="noopener" target="_blank">
${link.name}
</a>
`;
diff --git a/polygerrit-ui/app/elements/admin/gr-repo-plugin-config/gr-repo-plugin-config.ts b/polygerrit-ui/app/elements/admin/gr-repo-plugin-config/gr-repo-plugin-config.ts
index f9f760c..d5e5027 100644
--- a/polygerrit-ui/app/elements/admin/gr-repo-plugin-config/gr-repo-plugin-config.ts
+++ b/polygerrit-ui/app/elements/admin/gr-repo-plugin-config/gr-repo-plugin-config.ts
@@ -136,7 +136,7 @@
return html` <gr-tooltip-content
has-tooltip
show-icon
- title="${option.info.description}"
+ title=${option.info.description}
>
${titleName}
</gr-tooltip-content>`;
@@ -147,7 +147,7 @@
return html`
<gr-plugin-config-array-editor
@plugin-config-option-changed=${this._handleArrayChange}
- .pluginOption="${option}"
+ .pluginOption=${option}
?disabled=${this.disabled || !option.info.editable}
></gr-plugin-config-array-editor>
`;
@@ -172,7 +172,7 @@
?disabled=${this.disabled || !option.info.editable}
>
${(option.info.permitted_values || []).map(
- value => html`<option value="${value}">${value}</option>`
+ value => html`<option value=${value}>${value}</option>`
)}
</select>
</gr-select>
@@ -185,13 +185,13 @@
return html`
<iron-input
@input=${this._handleStringChange}
- data-option-key="${option._key}"
+ data-option-key=${option._key}
>
<input
is="iron-input"
- .value="${option.info.value ?? ''}"
+ .value=${option.info.value ?? ''}
@input=${this._handleStringChange}
- data-option-key="${option._key}"
+ data-option-key=${option._key}
?disabled=${this.disabled || !option.info.editable}
/>
</iron-input>
diff --git a/polygerrit-ui/app/elements/admin/gr-rule-editor/gr-rule-editor.ts b/polygerrit-ui/app/elements/admin/gr-rule-editor/gr-rule-editor.ts
index bb885dd..1685ca4 100644
--- a/polygerrit-ui/app/elements/admin/gr-rule-editor/gr-rule-editor.ts
+++ b/polygerrit-ui/app/elements/admin/gr-rule-editor/gr-rule-editor.ts
@@ -220,15 +220,13 @@
${this.renderMinAndMaxLabel()} ${this.renderMinAndMaxInput()}
<a
class="groupPath"
- href="${ifDefined(this.computeGroupPath(this.groupId))}"
+ href=${ifDefined(this.computeGroupPath(this.groupId))}
>
${this.groupName}
</a>
<gr-select
id="force"
- class="${this.computeForce(this.rule?.value?.action)
- ? 'force'
- : ''}"
+ class=${this.computeForce(this.rule?.value?.action) ? 'force' : ''}
.bindValue=${this.rule?.value?.force}
@bind-value-changed=${(e: BindValueChangeEvent) => {
this.handleForceBindValueChanged(e);
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list-action-bar/gr-change-list-action-bar.ts b/polygerrit-ui/app/elements/change-list/gr-change-list-action-bar/gr-change-list-action-bar.ts
index ba2e161..fdd7502 100644
--- a/polygerrit-ui/app/elements/change-list/gr-change-list-action-bar/gr-change-list-action-bar.ts
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list-action-bar/gr-change-list-action-bar.ts
@@ -109,6 +109,7 @@
: nothing}
</div>
<div class="actionButtons">
+ <gr-change-list-bulk-vote-flow></gr-change-list-bulk-vote-flow>
<gr-change-list-reviewer-flow></gr-change-list-reviewer-flow>
<gr-change-list-bulk-abandon-flow>
</gr-change-list-bulk-abandon-flow>
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list-action-bar/gr-change-list-action-bar_test.ts b/polygerrit-ui/app/elements/change-list/gr-change-list-action-bar/gr-change-list-action-bar_test.ts
index 5d05885..5804b99 100644
--- a/polygerrit-ui/app/elements/change-list/gr-change-list-action-bar/gr-change-list-action-bar_test.ts
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list-action-bar/gr-change-list-action-bar_test.ts
@@ -64,6 +64,7 @@
<span>1 change selected</span>
</div>
<div class="actionButtons">
+ <gr-change-list-bulk-vote-flow></gr-change-list-bulk-vote-flow>
<gr-change-list-reviewer-flow></gr-change-list-reviewer-flow>
<gr-change-list-bulk-abandon-flow>
</gr-change-list-bulk-abandon-flow>
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list-bulk-vote-flow/gr-change-list-bulk-vote-flow.ts b/polygerrit-ui/app/elements/change-list/gr-change-list-bulk-vote-flow/gr-change-list-bulk-vote-flow.ts
index fdf2b07..6790b15 100644
--- a/polygerrit-ui/app/elements/change-list/gr-change-list-bulk-vote-flow/gr-change-list-bulk-vote-flow.ts
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list-bulk-vote-flow/gr-change-list-bulk-vote-flow.ts
@@ -5,22 +5,36 @@
*/
import {customElement, query, state} from 'lit/decorators';
-import {LitElement, html, css} from 'lit';
+import {LitElement, html, css, nothing} from 'lit';
import {GrOverlay} from '../../shared/gr-overlay/gr-overlay';
import {resolve} from '../../../models/dependency';
import {bulkActionsModelToken} from '../../../models/bulk-actions/bulk-actions-model';
import {subscribe} from '../../lit/subscription-controller';
-import {ChangeInfo, AccountInfo} from '../../../api/rest-api';
+import {ChangeInfo, AccountInfo, NumericChangeId} from '../../../api/rest-api';
import {
getTriggerVotes,
computeLabels,
- mergeLabelMaps,
computeOrderedLabelValues,
+ mergeLabelInfoMaps,
+ getDefaultValue,
+ mergeLabelMaps,
+ Label,
+ StandardLabels,
} from '../../../utils/label-util';
import {getAppContext} from '../../../services/app-context';
import {fontStyles} from '../../../styles/gr-font-styles';
+import {queryAndAssert} from '../../../utils/common-util';
+import {
+ LabelNameToValuesMap,
+ ReviewInput,
+ LabelNameToValueMap,
+} from '../../../types/common';
+import {GrLabelScoreRow} from '../../change/gr-label-score-row/gr-label-score-row';
+import {ProgressStatus} from '../../../constants/constants';
+import {fireAlert, fireReload} from '../../../utils/event-util';
import '../../shared/gr-dialog/gr-dialog';
import '../../change/gr-label-score-row/gr-label-score-row';
+import {getOverallStatus} from '../../../utils/bulk-flow-util';
@customElement('gr-change-list-bulk-vote-flow')
export class GrChangeListBulkVoteFlow extends LitElement {
@@ -30,6 +44,8 @@
@state() selectedChanges: ChangeInfo[] = [];
+ @state() progressByChange: Map<NumericChangeId, ProgressStatus> = new Map();
+
@query('#actionOverlay') actionOverlay!: GrOverlay;
@state() account?: AccountInfo;
@@ -68,7 +84,10 @@
subscribe(
this,
this.getBulkActionsModel().selectedChanges$,
- selectedChanges => (this.selectedChanges = selectedChanges)
+ selectedChanges => {
+ this.selectedChanges = selectedChanges;
+ this.resetFlow();
+ }
);
subscribe(
this,
@@ -79,71 +98,204 @@
override render() {
const permittedLabels = this.computePermittedLabels();
- const labels = this.computeCommonLabels().filter(
- label =>
- permittedLabels?.[label.name] &&
- permittedLabels?.[label.name].length > 0
- );
- // TODO: disable button if no label can be voted upon
+ const triggerLabels = this.computeCommonTriggerLabels(permittedLabels);
+ const nonTriggerLabels = this.computeCommonPermittedLabels(
+ permittedLabels
+ ).filter(label => !triggerLabels.some(l => l.name === label.name));
return html`
- <gr-button flatten @click=${() => this.actionOverlay.open()}
+ <gr-button
+ .disabled=${triggerLabels.length === 0 && nonTriggerLabels.length === 0}
+ id="voteFlowButton"
+ flatten
+ @click=${() => this.actionOverlay.open()}
>Vote</gr-button
>
<gr-overlay id="actionOverlay" with-backdrop="">
<gr-dialog
- @cancel=${() => this.actionOverlay.close()}
+ .disableCancel=${!this.isCancelEnabled()}
+ .disabled=${!this.isConfirmEnabled()}
+ @confirm=${() => this.handleConfirm()}
+ @cancel=${() => this.handleClose()}
.cancelLabel=${'Close'}
>
<div slot="main">
- <div class="scoresTable newSubmitRequirements">
- <h3 class="heading-3">Submit requirements votes</h3>
- ${labels.map(
- label => html`<gr-label-score-row
- .label="${label}"
- .name="${label.name}"
- .labels="${labels}"
- .permittedLabels="${permittedLabels}"
- .orderedLabelValues="${computeOrderedLabelValues(
- permittedLabels
- )}"
- ></gr-label-score-row>`
- )}
- </div>
- <!-- TODO: Add section for trigger votes -->
+ ${this.renderLabels(
+ nonTriggerLabels,
+ 'Submit requirements votes',
+ permittedLabels
+ )}
+ ${this.renderLabels(
+ triggerLabels,
+ 'Trigger Votes',
+ permittedLabels
+ )}
</div>
+ <!-- TODO: Add error handling status if something fails -->
</gr-dialog>
</gr-overlay>
`;
}
+ private renderLabels(
+ labels: Label[],
+ heading: string,
+ permittedLabels?: LabelNameToValuesMap
+ ) {
+ return html` <div class="scoresTable newSubmitRequirements">
+ <h3 class="heading-3">${labels.length ? heading : nothing}</h3>
+ ${labels
+ .filter(
+ label =>
+ permittedLabels?.[label.name] &&
+ permittedLabels?.[label.name].length > 0
+ )
+ .map(
+ label => html`<gr-label-score-row
+ .label=${label}
+ .name=${label.name}
+ .labels=${this.computeLabelNameToInfoMap()}
+ .permittedLabels=${permittedLabels}
+ .orderedLabelValues=${computeOrderedLabelValues(permittedLabels)}
+ ></gr-label-score-row>`
+ )}
+ </div>`;
+ }
+
+ private resetFlow() {
+ this.progressByChange = new Map(
+ this.selectedChanges.map(change => [
+ change._number,
+ ProgressStatus.NOT_STARTED,
+ ])
+ );
+ }
+
+ private isConfirmEnabled() {
+ // Action is allowed if none of the changes have any bulk action performed
+ // on them. In case an error happens then we keep the button disabled.
+ return (
+ getOverallStatus(this.progressByChange) === ProgressStatus.NOT_STARTED
+ );
+ }
+
+ private isCancelEnabled() {
+ return getOverallStatus(this.progressByChange) !== ProgressStatus.RUNNING;
+ }
+
+ private handleClose() {
+ this.actionOverlay.close();
+ if (getOverallStatus(this.progressByChange) === ProgressStatus.NOT_STARTED)
+ return;
+ fireAlert(this, 'Reloading page..');
+ fireReload(this, true);
+ }
+
+ private handleConfirm() {
+ this.progressByChange.clear();
+ const reviewInput: ReviewInput = {
+ labels: this.getLabelValues(
+ this.computeCommonPermittedLabels(this.computePermittedLabels())
+ ),
+ };
+ for (const change of this.selectedChanges) {
+ this.progressByChange.set(change._number, ProgressStatus.RUNNING);
+ }
+ this.requestUpdate();
+ const promises = this.getBulkActionsModel().voteChanges(reviewInput);
+ for (let index = 0; index < promises.length; index++) {
+ const changeNum = this.selectedChanges[index]._number;
+ promises[index]
+ .then(() => {
+ this.progressByChange.set(changeNum, ProgressStatus.SUCCESSFUL);
+ })
+ .catch(() => {
+ this.progressByChange.set(changeNum, ProgressStatus.FAILED);
+ })
+ .finally(() => {
+ this.requestUpdate();
+ });
+ }
+ }
+
+ // private but used in tests
+ getLabelValues(commonPermittedLabels: Label[]): LabelNameToValueMap {
+ const labels: LabelNameToValueMap = {};
+
+ for (const label of commonPermittedLabels) {
+ const selectorEl = queryAndAssert<GrLabelScoreRow>(
+ this,
+ `gr-label-score-row[name="${label.name}"]`
+ );
+ if (!selectorEl?.selectedItem) continue;
+
+ const selectedVal =
+ typeof selectorEl.selectedValue === 'string'
+ ? Number(selectorEl.selectedValue)
+ : selectorEl.selectedValue;
+
+ if (selectedVal === undefined) continue;
+
+ const defValNum = getDefaultValue(
+ this.selectedChanges[0].labels,
+ label.name
+ );
+ if (selectedVal !== defValNum) {
+ labels[label.name] = selectedVal;
+ }
+ }
+ return labels;
+ }
+
// private but used in tests
computePermittedLabels() {
// Reduce method for empty array throws error if no initial value specified
if (this.selectedChanges.length === 0) return {};
- return this.selectedChanges
+ const permittedLabels = this.selectedChanges
.map(changes => changes.permitted_labels)
.reduce(mergeLabelMaps);
+ // TODO: show a warning to the user that Code Review cannot be voted upon
+ if (permittedLabels?.[StandardLabels.CODE_REVIEW]) {
+ delete permittedLabels[StandardLabels.CODE_REVIEW];
+ }
+ return permittedLabels;
+ }
+
+ private computeLabelNameToInfoMap() {
+ // Reduce method for empty array throws error if no initial value specified
+ if (this.selectedChanges.length === 0) return {};
+
+ return this.selectedChanges
+ .map(changes => changes.labels)
+ .reduce(mergeLabelInfoMaps);
}
// private but used in tests
- computeNonTriggerLabels(change: ChangeInfo) {
- const triggerVotes = getTriggerVotes(change);
- const labels = computeLabels(this.account, change).filter(
- label => !triggerVotes.includes(label.name)
+ computeCommonTriggerLabels(permittedLabels?: LabelNameToValuesMap) {
+ if (this.selectedChanges.length === 0) return [];
+ const triggerVotes = this.selectedChanges
+ .map(change => getTriggerVotes(change))
+ .reduce((prev, current) =>
+ current.filter(label => prev.some(l => l === label))
+ );
+ return this.computeCommonPermittedLabels(permittedLabels).filter(label =>
+ triggerVotes.includes(label.name)
);
- return labels;
}
// private but used in tests
- // TODO: Remove Code Review label explicitly
- computeCommonLabels() {
+ computeCommonPermittedLabels(permittedLabels?: LabelNameToValuesMap) {
// Reduce method for empty array throws error if no initial value specified
if (this.selectedChanges.length === 0) return [];
return this.selectedChanges
- .map(change => this.computeNonTriggerLabels(change))
+ .map(change => computeLabels(this.account, change))
.reduce((prev, current) =>
current.filter(label => prev.some(l => l.name === label.name))
+ )
+ .filter(
+ label =>
+ permittedLabels?.[label.name] &&
+ permittedLabels?.[label.name].length > 0
);
}
}
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list-bulk-vote-flow/gr-change-list-bulk-vote-flow_test.ts b/polygerrit-ui/app/elements/change-list/gr-change-list-bulk-vote-flow/gr-change-list-bulk-vote-flow_test.ts
index 55b1ac3..5a965d9 100644
--- a/polygerrit-ui/app/elements/change-list/gr-change-list-bulk-vote-flow/gr-change-list-bulk-vote-flow_test.ts
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list-bulk-vote-flow/gr-change-list-bulk-vote-flow_test.ts
@@ -11,10 +11,17 @@
bulkActionsModelToken,
LoadingState,
} from '../../../models/bulk-actions/bulk-actions-model';
-import {waitUntilObserved, stubRestApi} from '../../../test/test-utils';
+import {
+ waitUntilObserved,
+ stubRestApi,
+ queryAndAssert,
+ query,
+ mockPromise,
+ queryAll,
+} from '../../../test/test-utils';
import {ChangeInfo, NumericChangeId, LabelInfo} from '../../../api/rest-api';
import {getAppContext} from '../../../services/app-context';
-import {fixture} from '@open-wc/testing-helpers';
+import {fixture, waitUntil} from '@open-wc/testing-helpers';
import {wrapInProvider} from '../../../models/di-provider-element';
import {html} from 'lit';
import {SinonStubbedMember} from 'sinon';
@@ -24,25 +31,62 @@
createSubmitRequirementResultInfo,
} from '../../../test/test-data-generators';
import './gr-change-list-bulk-vote-flow';
+import {GrButton} from '../../shared/gr-button/gr-button';
+import {ProgressStatus} from '../../../constants/constants';
+import {StandardLabels} from '../../../utils/label-util';
const change1: ChangeInfo = {
...createChange(),
_number: 1 as NumericChangeId,
permitted_labels: {
+ [StandardLabels.CODE_REVIEW]: ['-1', '0', '+1', '+2'],
A: ['-1', '0', '+1', '+2'],
B: ['-1', '0'],
C: ['-1', '0'],
- D: ['0'], // Does not exist on change2
+ change1OnlyLabelD: ['0'], // Does not exist on change2
+ change1OnlyTriggerLabelE: ['0'], // Does not exist on change2
},
+ labels: {
+ [StandardLabels.CODE_REVIEW]: {value: null} as LabelInfo,
+ A: {value: null} as LabelInfo,
+ B: {value: null} as LabelInfo,
+ C: {value: null} as LabelInfo,
+ change1OnlyLabelD: {value: null} as LabelInfo,
+ change1OnlyTriggerLabelE: {value: null} as LabelInfo,
+ },
+ submit_requirements: [
+ createSubmitRequirementResultInfo(
+ `label:${StandardLabels.CODE_REVIEW}=MAX`
+ ),
+ createSubmitRequirementResultInfo('label:A=MAX'),
+ createSubmitRequirementResultInfo('label:B=MAX'),
+ createSubmitRequirementResultInfo('label:C=MAX'),
+ createSubmitRequirementResultInfo('label:change1OnlyLabelD=MAX'),
+ ],
};
const change2: ChangeInfo = {
...createChange(),
_number: 2 as NumericChangeId,
permitted_labels: {
+ [StandardLabels.CODE_REVIEW]: ['-1', '0', '+1', '+2'],
A: ['-1', '0', '+1', '+2'], // Intersects fully with change1
B: ['0', ' +1'], // Intersects with change1 on 0
C: ['+1', '+2'], // Does not intersect with change1 at all
},
+ labels: {
+ [StandardLabels.CODE_REVIEW]: {value: null} as LabelInfo,
+ A: {value: null} as LabelInfo,
+ B: {value: null} as LabelInfo,
+ C: {value: null} as LabelInfo,
+ },
+ submit_requirements: [
+ createSubmitRequirementResultInfo(
+ `label:${StandardLabels.CODE_REVIEW}=MAX`
+ ),
+ createSubmitRequirementResultInfo('label:A=MAX'),
+ createSubmitRequirementResultInfo('label:B=MAX'),
+ createSubmitRequirementResultInfo('label:C=MAX'),
+ ],
};
suite('gr-change-list-bulk-vote-flow tests', () => {
@@ -77,16 +121,6 @@
});
test('renders', async () => {
- change1.labels = {
- a: {value: null} as LabelInfo,
- b: {value: null} as LabelInfo,
- c: {value: null} as LabelInfo,
- };
- change1.submit_requirements = [
- createSubmitRequirementResultInfo('label:a=MAX'),
- createSubmitRequirementResultInfo('label:b=MAX'),
- createSubmitRequirementResultInfo('label:c=MAX'),
- ];
const changes: ChangeInfo[] = [change1];
getChangesStub.returns(Promise.resolve(changes));
model.sync(changes);
@@ -99,6 +133,7 @@
expect(element).shadowDom.to.equal(/* HTML */ `<gr-button
aria-disabled="false"
flatten=""
+ id="voteFlowButton"
role="button"
tabindex="0"
>
@@ -115,12 +150,197 @@
<div slot="main">
<div class="newSubmitRequirements scoresTable">
<h3 class="heading-3">Submit requirements votes</h3>
+ <gr-label-score-row name="A"> </gr-label-score-row>
+ <gr-label-score-row name="B"> </gr-label-score-row>
+ <gr-label-score-row name="C"> </gr-label-score-row>
+ <gr-label-score-row name="change1OnlyLabelD">
+ </gr-label-score-row>
+ </div>
+ <div class="newSubmitRequirements scoresTable">
+ <h3 class="heading-3">Trigger Votes</h3>
+ <gr-label-score-row name="change1OnlyTriggerLabelE">
+ </gr-label-score-row>
</div>
</div>
</gr-dialog>
</gr-overlay> `);
});
+ test('button state updates as changes are updated', async () => {
+ const changes: ChangeInfo[] = [change1];
+ getChangesStub.returns(Promise.resolve(changes));
+ model.sync(changes);
+ await waitUntilObserved(
+ model.loadingState$,
+ state => state === LoadingState.LOADED
+ );
+ await selectChange(change1);
+ await element.updateComplete;
+ await flush();
+
+ assert.isFalse(
+ queryAndAssert<GrButton>(element, '#voteFlowButton').disabled
+ );
+
+ // No common label with change1 so button is disabled
+ change2.labels = {
+ x: {value: null} as LabelInfo,
+ y: {value: null} as LabelInfo,
+ z: {value: null} as LabelInfo,
+ };
+ change2.submit_requirements = [
+ createSubmitRequirementResultInfo('label:x=MAX'),
+ createSubmitRequirementResultInfo('label:y=MAX'),
+ createSubmitRequirementResultInfo('label:z=MAX'),
+ ];
+ changes.push({...change2});
+ getChangesStub.restore();
+ getChangesStub.returns(Promise.resolve(changes));
+ model.sync(changes);
+ await waitUntilObserved(
+ model.loadingState$,
+ state => state === LoadingState.LOADED
+ );
+ await selectChange(change2);
+ await element.updateComplete;
+
+ assert.isTrue(
+ queryAndAssert<GrButton>(element, '#voteFlowButton').disabled
+ );
+ });
+
+ test('progress updates as request is resolved', async () => {
+ const changes: ChangeInfo[] = [{...change1}];
+ getChangesStub.returns(Promise.resolve(changes));
+ model.sync(changes);
+ await waitUntilObserved(
+ model.loadingState$,
+ state => state === LoadingState.LOADED
+ );
+ await selectChange(change1);
+ await element.updateComplete;
+ const saveChangeReview = mockPromise<Response>();
+ stubRestApi('saveChangeReview').returns(saveChangeReview);
+
+ assert.isNotOk(
+ queryAndAssert<GrButton>(query(element, 'gr-dialog'), '#confirm').disabled
+ );
+ assert.isNotOk(
+ queryAndAssert<GrButton>(query(element, 'gr-dialog'), '#cancel').disabled
+ );
+
+ const scores = queryAll(element, 'gr-label-score-row');
+ queryAndAssert<GrButton>(scores[0], 'gr-button[data-value="+1"]').click();
+ queryAndAssert<GrButton>(scores[1], 'gr-button[data-value="-1"]').click();
+
+ await element.updateComplete;
+
+ assert.deepEqual(
+ element.getLabelValues(
+ element.computeCommonPermittedLabels(element.computePermittedLabels())
+ ),
+ {
+ A: 1,
+ B: -1,
+ }
+ );
+
+ queryAndAssert<GrButton>(query(element, 'gr-dialog'), '#confirm').click();
+ await element.updateComplete;
+
+ assert.isTrue(
+ queryAndAssert<GrButton>(query(element, 'gr-dialog'), '#confirm').disabled
+ );
+ assert.isTrue(
+ queryAndAssert<GrButton>(query(element, 'gr-dialog'), '#cancel').disabled
+ );
+
+ assert.equal(
+ element.progressByChange.get(1 as NumericChangeId),
+ ProgressStatus.RUNNING
+ );
+
+ saveChangeReview.resolve({...new Response(), status: 200});
+ await waitUntil(
+ () =>
+ element.progressByChange.get(1 as NumericChangeId) ===
+ ProgressStatus.SUCCESSFUL
+ );
+
+ assert.isTrue(
+ queryAndAssert<GrButton>(query(element, 'gr-dialog'), '#confirm').disabled
+ );
+ assert.isNotOk(
+ queryAndAssert<GrButton>(query(element, 'gr-dialog'), '#cancel').disabled
+ );
+
+ assert.equal(
+ element.progressByChange.get(1 as NumericChangeId),
+ ProgressStatus.SUCCESSFUL
+ );
+ });
+
+ suite('closing dialog triggers reloads', () => {
+ test('closing dialog triggers a reload', async () => {
+ const changes: ChangeInfo[] = [change1, change2];
+ getChangesStub.returns(Promise.resolve(changes));
+
+ const fireStub = sinon.stub(element, 'dispatchEvent');
+
+ stubRestApi('saveChangeReview').callsFake(
+ (_changeNum, _patchNum, _review, errFn) =>
+ Promise.resolve(new Response()).then(res => {
+ errFn && errFn();
+ return res;
+ })
+ );
+
+ model.sync(changes);
+ await waitUntilObserved(
+ model.loadingState$,
+ state => state === LoadingState.LOADED
+ );
+ await selectChange(change1);
+ await selectChange(change2);
+ await element.updateComplete;
+
+ queryAndAssert<GrButton>(query(element, 'gr-dialog'), '#confirm').click();
+
+ await waitUntil(
+ () =>
+ element.progressByChange.get(2 as NumericChangeId) ===
+ ProgressStatus.FAILED
+ );
+
+ assert.isFalse(fireStub.called);
+
+ queryAndAssert<GrButton>(query(element, 'gr-dialog'), '#cancel').click();
+
+ await waitUntil(() => fireStub.called);
+ assert.equal(fireStub.lastCall.args[0].type, 'reload');
+ });
+
+ test('closing dialog does not trigger reload if no request made', async () => {
+ const changes: ChangeInfo[] = [change1, change2];
+ getChangesStub.returns(Promise.resolve(changes));
+
+ const fireStub = sinon.stub(element, 'dispatchEvent');
+
+ model.sync(changes);
+ await waitUntilObserved(
+ model.loadingState$,
+ state => state === LoadingState.LOADED
+ );
+ await selectChange(change1);
+ await selectChange(change2);
+ await element.updateComplete;
+
+ queryAndAssert<GrButton>(query(element, 'gr-dialog'), '#cancel').click();
+
+ assert.isFalse(fireStub.called);
+ });
+ });
+
test('computePermittedLabels', async () => {
// {} if no change is selected
assert.deepEqual(element.computePermittedLabels(), {});
@@ -139,7 +359,8 @@
A: ['-1', '0', '+1', '+2'],
B: ['-1', '0'],
C: ['-1', '0'],
- D: ['0'],
+ change1OnlyLabelD: ['0'],
+ change1OnlyTriggerLabelE: ['0'],
});
changes.push(change2);
@@ -159,61 +380,43 @@
});
});
- test('computeCommonLabels', async () => {
- const change3: ChangeInfo = {
- ...createChange(),
- _number: 3 as NumericChangeId,
- };
- const change4: ChangeInfo = {
- ...createChange(),
- _number: 4 as NumericChangeId,
+ test('computeCommonPermittedLabels', async () => {
+ const createChangeWithLabels = (
+ num: NumericChangeId,
+ labelNames: string[],
+ triggerLabels?: string[]
+ ) => {
+ const change = createChange();
+ change._number = num;
+ change.submit_requirements = [];
+ change.labels = {};
+ change.permitted_labels = {};
+ for (const label of labelNames) {
+ change.labels[label] = {value: null} as LabelInfo;
+ if (!triggerLabels?.includes(label)) {
+ change.submit_requirements.push(
+ createSubmitRequirementResultInfo(`label:${label}=MAX`)
+ );
+ }
+ change.permitted_labels[label] = ['0'];
+ }
+ return change;
};
- change1.labels = {
- a: {value: null} as LabelInfo,
- b: {value: null} as LabelInfo,
- c: {value: null} as LabelInfo,
- };
- change1.submit_requirements = [
- createSubmitRequirementResultInfo('label:a=MAX'),
- createSubmitRequirementResultInfo('label:b=MAX'),
- createSubmitRequirementResultInfo('label:c=MAX'),
+ const changes: ChangeInfo[] = [
+ createChangeWithLabels(
+ 1 as NumericChangeId,
+ ['a', 'triggerLabelB', 'c'],
+ ['triggerLabelB']
+ ),
+ createChangeWithLabels(
+ 2 as NumericChangeId,
+ ['triggerLabelB', 'c', 'd'],
+ ['triggerLabelB']
+ ),
+ createChangeWithLabels(3 as NumericChangeId, ['c', 'd', 'e']),
+ createChangeWithLabels(4 as NumericChangeId, ['x', 'y', 'z']),
];
-
- change2.labels = {
- b: {value: null} as LabelInfo,
- c: {value: null} as LabelInfo,
- d: {value: null} as LabelInfo,
- };
- change2.submit_requirements = [
- createSubmitRequirementResultInfo('label:b=MAX'),
- createSubmitRequirementResultInfo('label:c=MAX'),
- createSubmitRequirementResultInfo('label:d=MAX'),
- ];
-
- change3.labels = {
- c: {value: null} as LabelInfo,
- d: {value: null} as LabelInfo,
- e: {value: null} as LabelInfo,
- };
- change3.submit_requirements = [
- createSubmitRequirementResultInfo('label:c=MAX'),
- createSubmitRequirementResultInfo('label:d=MAX'),
- createSubmitRequirementResultInfo('label:e=MAX'),
- ];
-
- change4.labels = {
- x: {value: null} as LabelInfo,
- y: {value: null} as LabelInfo,
- z: {value: null} as LabelInfo,
- };
- change4.submit_requirements = [
- createSubmitRequirementResultInfo('label:x=MAX'),
- createSubmitRequirementResultInfo('label:y=MAX'),
- createSubmitRequirementResultInfo('label:z=MAX'),
- ];
-
- const changes: ChangeInfo[] = [change1, change2, change3, change4];
// Labels for each change are [a,b,c] [b,c,d] [c,d,e] [x,y,z]
getChangesStub.returns(Promise.resolve(changes));
model.sync(changes);
@@ -222,34 +425,77 @@
model.loadingState$,
state => state === LoadingState.LOADED
);
- await selectChange(change1);
+ await selectChange(
+ createChangeWithLabels(1 as NumericChangeId, ['a', 'triggerLabelB', 'c'])
+ );
await element.updateComplete;
- assert.deepEqual(element.computeCommonLabels(), [
- {name: 'a', value: null},
- {name: 'b', value: null},
- {name: 'c', value: null},
- ]);
+ // Code-Review is not a common permitted label
+ assert.deepEqual(
+ element.computeCommonPermittedLabels(element.computePermittedLabels()),
+ [
+ {name: 'a', value: null},
+ {name: 'c', value: null},
+ {name: 'triggerLabelB', value: null},
+ ]
+ );
- await selectChange(change2);
+ await selectChange(
+ createChangeWithLabels(2 as NumericChangeId, ['triggerLabelB', 'c', 'd'])
+ );
+ assert.deepEqual(
+ element.computeCommonTriggerLabels(element.computePermittedLabels()),
+ [{name: 'triggerLabelB', value: null}]
+ );
+
await element.updateComplete;
- // Intersection of [a,b,c] [b,c,d] is [b,c]
- assert.deepEqual(element.computeCommonLabels(), [
- {name: 'b', value: null},
- {name: 'c', value: null},
- ]);
+ // Intersection of [CR, 'a', 'triggerLabelB', 'c']
+ // [CR, 'triggerLabelB', 'c', 'd'] is [triggerLabelB,c]
+ // Code-Review is not a common permitted label
+ assert.deepEqual(
+ element.computeCommonPermittedLabels(element.computePermittedLabels()),
+ [
+ {name: 'c', value: null},
+ {name: 'triggerLabelB', value: null},
+ ]
+ );
+ assert.deepEqual(
+ element.computeCommonTriggerLabels(element.computePermittedLabels()),
+ [{name: 'triggerLabelB', value: null}]
+ );
- await selectChange(change3);
+ await selectChange(
+ createChangeWithLabels(3 as NumericChangeId, ['c', 'd', 'e'])
+ );
+
await element.updateComplete;
- // Intersection of [a,b,c] [b,c,d] [c,d,e] is [c]
- assert.deepEqual(element.computeCommonLabels(), [{name: 'c', value: null}]);
+ // Intersection of [a,triggerLabelB,c] [triggerLabelB,c,d] [c,d,e] is [c]
+ assert.deepEqual(
+ element.computeCommonPermittedLabels(element.computePermittedLabels()),
+ [{name: 'c', value: null}]
+ );
+ assert.deepEqual(
+ element.computeCommonTriggerLabels(element.computePermittedLabels()),
+ []
+ );
- await selectChange(change4);
+ await selectChange(
+ createChangeWithLabels(4 as NumericChangeId, ['x', 'y', 'z'])
+ );
+ assert.deepEqual(
+ element.computeCommonTriggerLabels(element.computePermittedLabels()),
+ []
+ );
+
await element.updateComplete;
- // Intersection of [a,b,c] [b,c,d] [c,d,e] [x,y,z] is []
- assert.deepEqual(element.computeCommonLabels(), []);
+ // Intersection of [a,triggerLabelB,c] [triggerLabelB,c,d] [c,d,e] [x,y,z]
+ // is []
+ assert.deepEqual(
+ element.computeCommonPermittedLabels(element.computePermittedLabels()),
+ []
+ );
});
});
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list-column-requirement/gr-change-list-column-requirement.ts b/polygerrit-ui/app/elements/change-list/gr-change-list-column-requirement/gr-change-list-column-requirement.ts
index 1b8921e..09b5cc9 100644
--- a/polygerrit-ui/app/elements/change-list/gr-change-list-column-requirement/gr-change-list-column-requirement.ts
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list-column-requirement/gr-change-list-column-requirement.ts
@@ -74,7 +74,7 @@
override render() {
return html`<div
class="container ${this.computeClass()}"
- title="${ifDefined(this.computeLabelTitle())}"
+ title=${ifDefined(this.computeLabelTitle())}
>
${this.renderContent()}
</div>`;
@@ -105,14 +105,14 @@
if (votes.length > 0) {
const bestVote = votes[0];
return html`<gr-vote-chip
- .vote="${bestVote}"
- .label="${labelInfo}"
+ .vote=${bestVote}
+ .label=${labelInfo}
tooltip-with-who-voted
></gr-vote-chip>`;
}
}
if (isQuickLabelInfo(labelInfo)) {
- return html`<gr-vote-chip .label="${labelInfo}"></gr-vote-chip>`;
+ return html`<gr-vote-chip .label=${labelInfo}></gr-vote-chip>`;
}
return;
}
@@ -144,8 +144,8 @@
return this.renderStatusIcon(requirement.status);
} else {
return html`<gr-vote-chip
- .vote="${worstVote}"
- .label="${labelInfo}"
+ .vote=${worstVote}
+ .label=${labelInfo}
tooltip-with-who-voted
></gr-vote-chip>`;
}
@@ -153,10 +153,7 @@
private renderStatusIcon(status: SubmitRequirementStatus) {
const icon = iconForStatus(status ?? SubmitRequirementStatus.ERROR);
- return html`<iron-icon
- class="${icon}"
- icon="gr-icons:${icon}"
- ></iron-icon>`;
+ return html`<iron-icon class=${icon} icon="gr-icons:${icon}"></iron-icon>`;
}
private computeClass(): string {
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list-column-requirements-summary/gr-change-list-column-requirements-summary.ts b/polygerrit-ui/app/elements/change-list/gr-change-list-column-requirements-summary/gr-change-list-column-requirements-summary.ts
index 2272133..1f8bf51 100644
--- a/polygerrit-ui/app/elements/change-list/gr-change-list-column-requirements-summary/gr-change-list-column-requirements-summary.ts
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list-column-requirements-summary/gr-change-list-column-requirements-summary.ts
@@ -122,10 +122,10 @@
}
renderState(icon: string, aggregation: string | TemplateResult) {
- return html`<span class="${icon}" role="button" tabindex="0">
+ return html`<span class=${icon} role="button" tabindex="0">
<gr-submit-requirement-dashboard-hovercard .change=${this.change}>
</gr-submit-requirement-dashboard-hovercard>
- <iron-icon class="${icon}" icon="gr-icons:${icon}" role="img"></iron-icon
+ <iron-icon class=${icon} icon="gr-icons:${icon}" role="img"></iron-icon
>${aggregation}</span
>`;
}
@@ -142,10 +142,10 @@
return html`<iron-icon
icon="gr-icons:comment"
class="commentIcon"
- .title="${pluralize(
+ .title=${pluralize(
this.change?.unresolved_comment_count,
'unresolved comment'
- )}"
+ )}
></iron-icon>`;
}
}
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 77602d0..15532d2 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
@@ -340,7 +340,7 @@
return html`
<td class="cell number">
- <a href="${changeUrl}">${this.change?._number}</a>
+ <a href=${changeUrl}>${this.change?._number}</a>
</td>
`;
}
@@ -352,8 +352,8 @@
return html`
<td class="cell subject">
<a
- title="${ifDefined(this.change?.subject)}"
- href="${changeUrl}"
+ title=${ifDefined(this.change?.subject)}
+ href=${changeUrl}
@click=${() => this.handleChangeClick()}
>
<div class="container">
@@ -413,7 +413,7 @@
this.renderChangeReviewers(reviewer, index)
)}
${this.computeAdditionalReviewersCount()
- ? html`<span title="${this.computeAdditionalReviewersTitle()}"
+ ? html`<span title=${this.computeAdditionalReviewersTitle()}
>+${this.computeAdditionalReviewersCount()}</span
>`
: ''}
@@ -460,13 +460,13 @@
return html`
<td class="cell repo">
- <a class="fullRepo" href="${this.computeRepoUrl()}">
+ <a class="fullRepo" href=${this.computeRepoUrl()}>
${this.computeRepoDisplay()}
</a>
<a
class="truncatedRepo"
- href="${this.computeRepoUrl()}"
- title="${this.computeRepoDisplay()}"
+ href=${this.computeRepoUrl()}
+ title=${this.computeRepoDisplay()}
>
${this.computeTruncatedRepoDisplay()}
</a>
@@ -480,7 +480,7 @@
return html`
<td class="cell branch">
- <a href="${this.computeRepoBranchURL()}"> ${this.change?.branch} </a>
+ <a href=${this.computeRepoBranchURL()}> ${this.change?.branch} </a>
${this.renderChangeBranch()}
</td>
`;
@@ -490,7 +490,7 @@
if (!this.change?.topic) return;
return html`
- (<a href="${this.computeTopicURL()}"
+ (<a href=${this.computeTopicURL()}
><!--
--><gr-limited-text .limit=${50} .text=${this.change.topic}>
</gr-limited-text
@@ -550,7 +550,7 @@
return html`
<td class="cell size">
- <gr-tooltip-content has-tooltip title="${this.computeSizeTooltip()}">
+ <gr-tooltip-content has-tooltip title=${this.computeSizeTooltip()}>
${this.renderChangeSize()}
</gr-tooltip-content>
</td>
@@ -590,8 +590,8 @@
}
return html`
<td
- title="${this.computeLabelTitle(labelName)}"
- class="${this.computeLabelClass(labelName)}"
+ title=${this.computeLabelTitle(labelName)}
+ class=${this.computeLabelClass(labelName)}
>
${this.renderChangeHasLabelIcon(labelName)}
</td>
@@ -610,7 +610,7 @@
private renderChangePluginEndpoint(pluginEndpointName: string) {
return html`
<td class="cell endpoint">
- <gr-endpoint-decorator name="${pluginEndpointName}">
+ <gr-endpoint-decorator name=${pluginEndpointName}>
<gr-endpoint-param name="change" .value=${this.change}>
</gr-endpoint-param>
</gr-endpoint-decorator>
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list-reviewer-flow/gr-change-list-reviewer-flow.ts b/polygerrit-ui/app/elements/change-list/gr-change-list-reviewer-flow/gr-change-list-reviewer-flow.ts
index b504ea22..458fe9a 100644
--- a/polygerrit-ui/app/elements/change-list/gr-change-list-reviewer-flow/gr-change-list-reviewer-flow.ts
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list-reviewer-flow/gr-change-list-reviewer-flow.ts
@@ -3,12 +3,12 @@
* Copyright 2022 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
-import {css, html, LitElement} from 'lit';
+import {css, html, LitElement, nothing} from 'lit';
import {customElement, query, state} from 'lit/decorators';
import {ProgressStatus, ReviewerState} from '../../../constants/constants';
import {bulkActionsModelToken} from '../../../models/bulk-actions/bulk-actions-model';
import {resolve} from '../../../models/dependency';
-import {AccountInfo, ChangeInfo} from '../../../types/common';
+import {AccountInfo, ChangeInfo, NumericChangeId} from '../../../types/common';
import {subscribe} from '../../lit/subscription-controller';
import '../../shared/gr-overlay/gr-overlay';
import '../../shared/gr-dialog/gr-dialog';
@@ -20,6 +20,7 @@
SUGGESTIONS_PROVIDERS_USERS_TYPES,
} from '../../../scripts/gr-reviewer-suggestions-provider/gr-reviewer-suggestions-provider';
import '../../shared/gr-account-list/gr-account-list';
+import {getOverallStatus} from '../../../utils/bulk-flow-util';
const SUGGESTIONS_PROVIDERS_USERS_TYPES_BY_REVIEWER_STATE: Record<
ReviewerState,
@@ -45,7 +46,12 @@
ReviewerSuggestionsProvider
> = new Map();
- @state() private progressByChange = new Map<ChangeInfo, ProgressStatus>();
+ @state() private progressByChangeNum = new Map<
+ NumericChangeId,
+ ProgressStatus
+ >();
+
+ @state() private isOverlayOpen = false;
@query('gr-overlay') private overlay!: GrOverlay;
@@ -77,45 +83,50 @@
this.getBulkActionsModel().selectedChanges$,
selectedChanges => {
this.selectedChanges = selectedChanges;
- this.resetFlow();
}
);
}
override render() {
- const overallStatus = this.getOverallStatus();
// TODO: factor out button+dialog component with promise-progress tracking
return html`
<gr-button
id="start-flow"
.disabled=${this.isFlowDisabled()}
flatten
- @click=${() => this.overlay.open()}
+ @click=${() => this.openOverlay()}
>add reviewer/cc</gr-button
>
<gr-overlay with-backdrop>
- <gr-dialog
- @cancel=${() => this.overlay.close()}
- @confirm=${() => this.onConfirm(overallStatus)}
- .confirmLabel=${this.getConfirmLabel(overallStatus)}
- .disabled=${overallStatus === ProgressStatus.RUNNING}
- >
- <div slot="header">Add Reviewer / CC</div>
- <div slot="main" class="grid">
- <span>Reviewers</span>
- ${this.renderAccountList(
- ReviewerState.REVIEWER,
- 'reviewer-list',
- 'Add reviewer'
- )}
- <span>CC</span>
- ${this.renderAccountList(ReviewerState.CC, 'cc-list', 'Add CC')}
- </div>
- </gr-dialog>
+ ${this.isOverlayOpen ? this.renderDialog() : nothing}
</gr-overlay>
`;
}
+ private renderDialog() {
+ const overallStatus = getOverallStatus(this.progressByChangeNum);
+ return html`
+ <gr-dialog
+ @cancel=${() => this.closeOverlay()}
+ @confirm=${() => this.onConfirm(overallStatus)}
+ .confirmLabel=${this.getConfirmLabel(overallStatus)}
+ .disabled=${overallStatus === ProgressStatus.RUNNING}
+ >
+ <div slot="header">Add Reviewer / CC</div>
+ <div slot="main" class="grid">
+ <span>Reviewers</span>
+ ${this.renderAccountList(
+ ReviewerState.REVIEWER,
+ 'reviewer-list',
+ 'Add reviewer'
+ )}
+ <span>CC</span>
+ ${this.renderAccountList(ReviewerState.CC, 'cc-list', 'Add CC')}
+ </div>
+ </gr-dialog>
+ `;
+ }
+
private renderAccountList(
reviewerState: ReviewerState,
id: string,
@@ -140,9 +151,23 @@
`;
}
+ private openOverlay() {
+ this.resetFlow();
+ this.isOverlayOpen = true;
+ this.overlay.open();
+ }
+
+ private closeOverlay() {
+ this.isOverlayOpen = false;
+ this.overlay.close();
+ }
+
private resetFlow() {
- this.progressByChange = new Map(
- this.selectedChanges.map(change => [change, ProgressStatus.NOT_STARTED])
+ this.progressByChangeNum = new Map(
+ this.selectedChanges.map(change => [
+ change._number,
+ ProgressStatus.NOT_STARTED,
+ ])
);
for (const state of [ReviewerState.REVIEWER, ReviewerState.CC]) {
this.updatedAccountsByReviewerState.set(
@@ -162,17 +187,23 @@
private onConfirm(overallStatus: ProgressStatus) {
switch (overallStatus) {
case ProgressStatus.NOT_STARTED:
- this.saveChanges();
+ this.saveReviewers();
break;
case ProgressStatus.SUCCESSFUL:
this.overlay.close();
break;
+ case ProgressStatus.FAILED:
+ this.overlay.close();
+ break;
}
}
- private saveChanges() {
- this.progressByChange = new Map(
- this.selectedChanges.map(change => [change, ProgressStatus.RUNNING])
+ private saveReviewers() {
+ this.progressByChangeNum = new Map(
+ this.selectedChanges.map(change => [
+ change._number,
+ ProgressStatus.RUNNING,
+ ])
);
const inFlightActions = this.getBulkActionsModel().addReviewers(
this.updatedAccountsByReviewerState
@@ -181,11 +212,14 @@
const change = this.selectedChanges[index];
inFlightActions[index]
.then(() => {
- this.progressByChange.set(change, ProgressStatus.SUCCESSFUL);
+ this.progressByChangeNum.set(
+ change._number,
+ ProgressStatus.SUCCESSFUL
+ );
this.requestUpdate();
})
.catch(() => {
- this.progressByChange.set(change, ProgressStatus.FAILED);
+ this.progressByChangeNum.set(change._number, ProgressStatus.FAILED);
this.requestUpdate();
});
}
@@ -230,17 +264,6 @@
suggestionsProvider.init();
return suggestionsProvider;
}
-
- private getOverallStatus() {
- const statuses = Array.from(this.progressByChange.values());
- if (statuses.every(s => s === ProgressStatus.NOT_STARTED)) {
- return ProgressStatus.NOT_STARTED;
- }
- if (statuses.some(s => s === ProgressStatus.RUNNING)) {
- return ProgressStatus.RUNNING;
- }
- return ProgressStatus.SUCCESSFUL;
- }
}
declare global {
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list-reviewer-flow/gr-change-list-reviewer-flow_test.ts b/polygerrit-ui/app/elements/change-list/gr-change-list-reviewer-flow/gr-change-list-reviewer-flow_test.ts
index ffb21af..afc7b4b 100644
--- a/polygerrit-ui/app/elements/change-list/gr-change-list-reviewer-flow/gr-change-list-reviewer-flow_test.ts
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list-reviewer-flow/gr-change-list-reviewer-flow_test.ts
@@ -89,7 +89,7 @@
await element.updateComplete;
});
- test('renders flow', async () => {
+ test('skips dialog render when closed', async () => {
expect(element).shadowDom.to.equal(/* HTML */ `
<gr-button
id="start-flow"
@@ -104,17 +104,7 @@
with-backdrop=""
tabindex="-1"
style="outline: none; display: none;"
- >
- <gr-dialog role="dialog">
- <div slot="header">Add Reviewer / CC</div>
- <div slot="main" class="grid">
- <span>Reviewers</span>
- <gr-account-list id="reviewer-list"></gr-account-list>
- <span>CC</span>
- <gr-account-list id="cc-list"></gr-account-list>
- </div>
- </gr-dialog>
- </gr-overlay>
+ ></gr-overlay>
`);
});
@@ -175,6 +165,34 @@
await dialog.updateComplete;
});
+ test('renders dialog when opened', async () => {
+ expect(element).shadowDom.to.equal(/* HTML */ `
+ <gr-button
+ id="start-flow"
+ flatten=""
+ aria-disabled="false"
+ role="button"
+ tabindex="0"
+ >add reviewer/cc</gr-button
+ >
+ <gr-overlay
+ with-backdrop=""
+ tabindex="-1"
+ style="outline: none; display: none;"
+ >
+ <gr-dialog role="dialog">
+ <div slot="header">Add Reviewer / CC</div>
+ <div slot="main" class="grid">
+ <span>Reviewers</span>
+ <gr-account-list id="reviewer-list"></gr-account-list>
+ <span>CC</span>
+ <gr-account-list id="cc-list"></gr-account-list>
+ </div>
+ </gr-dialog>
+ </gr-overlay>
+ `);
+ });
+
test('only lists reviewers/CCs shared by all changes', async () => {
const reviewerList = queryAndAssert<GrAccountList>(
dialog,
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 26ee1e1..85ea644 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
@@ -166,11 +166,9 @@
?aria-hidden=${!this.showStar}
?hidden=${!this.showStar}
></td>
- <td class="cell" colspan="${colSpan}">
+ <td class="cell" colspan=${colSpan}>
${this.changeSection.emptyStateSlotName
- ? html`<slot
- name="${this.changeSection.emptyStateSlotName}"
- ></slot>`
+ ? html`<slot name=${this.changeSection.emptyStateSlotName}></slot>`
: 'No changes'}
</td>
</tr>
@@ -191,10 +189,10 @@
<td aria-hidden="true" class="leftPadding"></td>
${this.renderSelectionHeader()}
<td aria-hidden="true" class="star" ?hidden=${!this.showStar}></td>
- <td class="cell" colspan="${colSpan}">
+ <td class="cell" colspan=${colSpan}>
<h2 class="heading-3">
<a
- href="${this.sectionHref(this.changeSection.query)}"
+ href=${this.sectionHref(this.changeSection.query)}
class="section-title"
>
<span class="section-name">${this.changeSection.name}</span>
@@ -240,12 +238,12 @@
}
private renderHeaderCell(item: string) {
- return html`<td class="${item.toLowerCase()}">${item}</td>`;
+ return html`<td class=${item.toLowerCase()}>${item}</td>`;
}
private renderLabelHeader(labelName: string) {
return html`
- <td class="label" title="${labelName}">
+ <td class="label" title=${labelName}>
${computeLabelShortcut(labelName)}
</td>
`;
@@ -254,7 +252,7 @@
private renderEndpointHeader(pluginHeader: string) {
return html`
<td class="endpoint">
- <gr-endpoint-decorator .name="${pluginHeader}"></gr-endpoint-decorator>
+ <gr-endpoint-decorator .name=${pluginHeader}></gr-endpoint-decorator>
</td>
`;
}
@@ -279,7 +277,7 @@
?showStar=${this.showStar}
tabindex=${ifDefined(tabindex)}
.labelNames=${this.labelNames}
- aria-label="${ariaLabel}"
+ aria-label=${ariaLabel}
></gr-change-list-item>
`;
}
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view.ts b/polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view.ts
index 7424cf6..85d53d7 100644
--- a/polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view.ts
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view.ts
@@ -248,7 +248,7 @@
if (this.offset === 0) return;
return html`
- <a id="prevArrow" href="${this.computeNavLink(-1)}">
+ <a id="prevArrow" href=${this.computeNavLink(-1)}>
<iron-icon icon="gr-icons:chevron-left" aria-label="Older"> </iron-icon>
</a>
`;
@@ -264,7 +264,7 @@
return;
return html`
- <a id="nextArrow" href="${this.computeNavLink(1)}">
+ <a id="nextArrow" href=${this.computeNavLink(1)}>
<iron-icon icon="gr-icons:chevron-right" aria-label="Newer">
</iron-icon>
</a>
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list.ts b/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list.ts
index 4cfd2d4..2c10c1e 100644
--- a/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list.ts
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list.ts
@@ -274,8 +274,8 @@
>
${changeSection.emptyStateSlotName
? html`<slot
- slot="${changeSection.emptyStateSlotName}"
- name="${changeSection.emptyStateSlotName}"
+ slot=${changeSection.emptyStateSlotName}
+ name=${changeSection.emptyStateSlotName}
></slot>`
: nothing}
</gr-change-list-section>
diff --git a/polygerrit-ui/app/elements/change-list/gr-create-commands-dialog/gr-create-commands-dialog.ts b/polygerrit-ui/app/elements/change-list/gr-create-commands-dialog/gr-create-commands-dialog.ts
index 6c9fa68..c41ad70 100644
--- a/polygerrit-ui/app/elements/change-list/gr-create-commands-dialog/gr-create-commands-dialog.ts
+++ b/polygerrit-ui/app/elements/change-list/gr-create-commands-dialog/gr-create-commands-dialog.ts
@@ -78,11 +78,9 @@
</li>
<li>
<p>If you are making a new commit use</p>
- <gr-shell-command
- .command="${Commands.CREATE}"
- ></gr-shell-command>
+ <gr-shell-command .command=${Commands.CREATE}></gr-shell-command>
<p>Or to amend an existing commit use</p>
- <gr-shell-command .command="${Commands.AMEND}"></gr-shell-command>
+ <gr-shell-command .command=${Commands.AMEND}></gr-shell-command>
<p>
Please make sure you add a commit message as it becomes the
description for your change.
@@ -91,7 +89,7 @@
<li>
<p>Push the change for code review</p>
<gr-shell-command
- .command="${Commands.PUSH_PREFIX + (this.branch ?? '[BRANCH]')}"
+ .command=${Commands.PUSH_PREFIX + (this.branch ?? '[BRANCH]')}
></gr-shell-command>
</li>
<li>
diff --git a/polygerrit-ui/app/elements/change-list/gr-dashboard-view/gr-dashboard-view.ts b/polygerrit-ui/app/elements/change-list/gr-dashboard-view/gr-dashboard-view.ts
index e6bae6e..e8b25dd 100644
--- a/polygerrit-ui/app/elements/change-list/gr-dashboard-view/gr-dashboard-view.ts
+++ b/polygerrit-ui/app/elements/change-list/gr-dashboard-view/gr-dashboard-view.ts
@@ -232,7 +232,7 @@
<div class="banner">
<div>
You have draft comments on closed changes.
- <a href="${this.computeDraftsLink()}" target="_blank">(view all)</a>
+ <a href=${this.computeDraftsLink()} target="_blank">(view all)</a>
</div>
<div>
<gr-button
diff --git a/polygerrit-ui/app/elements/change-list/gr-repo-header/gr-repo-header.ts b/polygerrit-ui/app/elements/change-list/gr-repo-header/gr-repo-header.ts
index d82ff7f..dec1656 100644
--- a/polygerrit-ui/app/elements/change-list/gr-repo-header/gr-repo-header.ts
+++ b/polygerrit-ui/app/elements/change-list/gr-repo-header/gr-repo-header.ts
@@ -62,7 +62,7 @@
return html`<div>
<span class="browse">Browse:</span>
${webLinks.map(
- link => html`<a target="_blank" href="${link.url}">${link.name}</a> `
+ link => html`<a target="_blank" href=${link.url}>${link.name}</a> `
)}
</div> `;
}
@@ -72,7 +72,7 @@
<h1 class="heading-1">${this.repo}</h1>
<hr />
<div>
- <span>Detail:</span> <a href="${this._repoUrl!}">Repo settings</a>
+ <span>Detail:</span> <a href=${this._repoUrl!}>Repo settings</a>
</div>
${this._renderLinks(this._webLinks)}
</div>`;
diff --git a/polygerrit-ui/app/elements/change-list/gr-user-header/gr-user-header.ts b/polygerrit-ui/app/elements/change-list/gr-user-header/gr-user-header.ts
index 7265a7f..72f2a44 100644
--- a/polygerrit-ui/app/elements/change-list/gr-user-header/gr-user-header.ts
+++ b/polygerrit-ui/app/elements/change-list/gr-user-header/gr-user-header.ts
@@ -65,7 +65,7 @@
override render() {
return html`<gr-avatar
- .account="${this._accountDetails}"
+ .account=${this._accountDetails}
.imageSize=${100}
aria-label="Account avatar"
></gr-avatar>
@@ -85,31 +85,31 @@
<div>
<span>Joined:</span>
<gr-date-formatter
- dateStr="${this._computeDetail(
+ dateStr=${this._computeDetail(
this._accountDetails,
'registered_on'
- )}"
+ )}
>
</gr-date-formatter>
</div>
<gr-endpoint-decorator name="user-header">
<gr-endpoint-param
name="accountDetails"
- .value="${this._accountDetails}"
+ .value=${this._accountDetails}
>
</gr-endpoint-param>
- <gr-endpoint-param name="loggedIn" .value="${this.loggedIn}">
+ <gr-endpoint-param name="loggedIn" .value=${this.loggedIn}>
</gr-endpoint-param>
</gr-endpoint-decorator>
</div>
<div class="info">
<div
- class="${this._computeDashboardLinkClass(
+ class=${this._computeDashboardLinkClass(
this.showDashboardLink,
this.loggedIn
- )}"
+ )}
>
- <a href="${this._computeDashboardUrl(this._accountDetails)}"
+ <a href=${this._computeDashboardUrl(this._accountDetails)}
>View dashboard</a
>
</div>
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 458d043..3ca7b0c 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
@@ -1220,7 +1220,7 @@
test('revert change with plugin hook', async () => {
const newRevertMsg = 'Modified revert msg';
sinon
- .stub(element.$.confirmRevertDialog, '_modifyRevertMsg')
+ .stub(element.$.confirmRevertDialog, 'modifyRevertMsg')
.callsFake(() => newRevertMsg);
element.change = {
...createChangeViewChange(),
@@ -1245,7 +1245,7 @@
sinon
.stub(
element.$.confirmRevertDialog,
- '_populateRevertSubmissionMessage'
+ 'populateRevertSubmissionMessage'
)
.callsFake(() => 'original msg');
await flush();
@@ -1255,7 +1255,7 @@
);
tap(revertButton);
await flush();
- assert.equal(element.$.confirmRevertDialog._message, newRevertMsg);
+ assert.equal(element.$.confirmRevertDialog.message, newRevertMsg);
});
suite('revert change submitted together', () => {
@@ -1319,7 +1319,7 @@
'\n' +
'23456:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa...' +
'\n';
- assert.equal(confirmRevertDialog._message, expectedMsg);
+ assert.equal(confirmRevertDialog.message, expectedMsg);
const radioInputs = queryAll(
confirmRevertDialog,
'input[name="revertOptions"]'
@@ -1330,7 +1330,7 @@
'Revert "random commit message"\n\nThis reverts ' +
'commit 2000.\n\nReason' +
' for revert: <INSERT REASONING HERE>\n';
- assert.equal(confirmRevertDialog._message, expectedMsg);
+ assert.equal(confirmRevertDialog.message, expectedMsg);
});
test('submit fails if message is not edited', async () => {
@@ -1348,7 +1348,7 @@
);
tap(confirmButton);
await flush();
- assert.isTrue(confirmRevertDialog._showErrorMessage);
+ assert.isTrue(confirmRevertDialog.showErrorMessage);
assert.isFalse(fireStub.called);
});
@@ -1379,20 +1379,20 @@
'Revert "random commit message"\n\nThis reverts ' +
'commit 2000.\n\nReason' +
' for revert: <INSERT REASONING HERE>\n';
- assert.equal(confirmRevertDialog._message, revertSubmissionMsg);
+ assert.equal(confirmRevertDialog.message, revertSubmissionMsg);
const newRevertMsg = revertSubmissionMsg + 'random';
const newSingleChangeMsg = singleChangeMsg + 'random';
- confirmRevertDialog._message = newRevertMsg;
+ confirmRevertDialog.message = newRevertMsg;
tap(radioInputs[0]);
await flush();
- assert.equal(confirmRevertDialog._message, singleChangeMsg);
- confirmRevertDialog._message = newSingleChangeMsg;
+ assert.equal(confirmRevertDialog.message, singleChangeMsg);
+ confirmRevertDialog.message = newSingleChangeMsg;
tap(radioInputs[1]);
await flush();
- assert.equal(confirmRevertDialog._message, newRevertMsg);
+ assert.equal(confirmRevertDialog.message, newRevertMsg);
tap(radioInputs[0]);
await flush();
- assert.equal(confirmRevertDialog._message, newSingleChangeMsg);
+ assert.equal(confirmRevertDialog.message, newSingleChangeMsg);
});
});
@@ -1430,7 +1430,7 @@
);
tap(confirmButton);
await flush();
- assert.isTrue(confirmRevertDialog._showErrorMessage);
+ assert.isTrue(confirmRevertDialog.showErrorMessage);
assert.isFalse(fireStub.called);
});
@@ -1451,9 +1451,9 @@
'Revert "random commit message"\n\n' +
'This reverts commit 2000.\n\nReason ' +
'for revert: <INSERT REASONING HERE>\n';
- assert.equal(confirmRevertDialog._message, msg);
+ assert.equal(confirmRevertDialog.message, msg);
let editedMsg = msg + 'hello';
- confirmRevertDialog._message += 'hello';
+ confirmRevertDialog.message += 'hello';
const confirmButton = queryAndAssert(
queryAndAssert(element.$.confirmRevertDialog, 'gr-dialog'),
'#confirm'
@@ -1462,7 +1462,7 @@
await flush();
// Contains generic template reason so doesn't submit
assert.isFalse(fireActionStub.called);
- confirmRevertDialog._message = confirmRevertDialog._message.replace(
+ confirmRevertDialog.message = confirmRevertDialog.message.replace(
'<INSERT REASONING HERE>',
''
);
diff --git a/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.ts b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.ts
index 2305a74..cd51627 100644
--- a/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.ts
+++ b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.ts
@@ -201,9 +201,6 @@
@property({type: Boolean, computed: '_computeIsWip(change)'})
_isWip = false;
- @property({type: String})
- _newHashtag?: Hashtag;
-
@property({type: Boolean})
_settingTopic = false;
@@ -338,17 +335,16 @@
return hasCherryPickOf;
}
- _handleHashtagChanged() {
+ _handleHashtagChanged(e: CustomEvent<string>) {
if (!this.change) {
throw new Error('change must be set');
}
- if (!this._newHashtag?.length) {
+ const newHashtag = e.detail.length ? e.detail : undefined;
+ if (!newHashtag?.length) {
return;
}
- const newHashtag = this._newHashtag;
- this._newHashtag = '' as Hashtag;
this.restApiService
- .setChangeHashtag(this.change._number, {add: [newHashtag]})
+ .setChangeHashtag(this.change._number, {add: [newHashtag as Hashtag]})
.then(newHashtag => {
this.set(['change', 'hashtags'], newHashtag);
fireEvent(this, 'hashtag-changed');
diff --git a/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata_html.ts b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata_html.ts
index 054f3f7..012c0d5 100644
--- a/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata_html.ts
+++ b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata_html.ts
@@ -300,7 +300,6 @@
mutable="[[_mutable]]"
reviewers-only=""
account="[[account]]"
- server-config="[[serverConfig]]"
></gr-reviewer-list>
</span>
</section>
@@ -314,7 +313,6 @@
mutable="[[_mutable]]"
ccs-only=""
account="[[account]]"
- server-config="[[serverConfig]]"
></gr-reviewer-list>
</span>
</section>
@@ -448,13 +446,13 @@
>
<gr-editable-label
class="topicEditableLabel"
- label-text="Add a topic"
+ labelText="Add a topic"
value="[[change.topic]]"
- max-length="1024"
+ maxLength="1024"
placeholder="[[_computeTopicPlaceholder(_topicReadOnly)]]"
read-only="[[_topicReadOnly]]"
on-changed="_handleTopicChanged"
- show-as-edit-pencil="true"
+ showAsEditPencil
autocomplete="true"
query="[[queryTopic]]"
></gr-editable-label>
@@ -506,12 +504,11 @@
<template is="dom-if" if="[[!_hashtagReadOnly]]">
<gr-editable-label
uppercase=""
- label-text="Add a hashtag"
- value="{{_newHashtag}}"
+ labelText="Add a hashtag"
placeholder="[[_computeHashtagPlaceholder(_hashtagReadOnly)]]"
read-only="[[_hashtagReadOnly]]"
on-changed="_handleHashtagChanged"
- show-as-edit-pencil="true"
+ showAsEditPencil
></gr-editable-label>
</template>
</span>
diff --git a/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata_test.ts b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata_test.ts
index 54c709a..37d5cb0 100644
--- a/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata_test.ts
+++ b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata_test.ts
@@ -846,12 +846,13 @@
test('changing hashtag', async () => {
await flush();
- element._newHashtag = 'new hashtag' as Hashtag;
const newHashtag: Hashtag[] = ['new hashtag' as Hashtag];
const setChangeHashtagStub = stubRestApi('setChangeHashtag').returns(
Promise.resolve(newHashtag)
);
- element._handleHashtagChanged();
+ element._handleHashtagChanged(
+ new CustomEvent('test', {detail: 'new hashtag'})
+ );
assert.isTrue(
setChangeHashtagStub.calledWith(42 as NumericChangeId, {
add: ['new hashtag' as Hashtag],
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 72a6b9c..f5893ac 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
@@ -155,8 +155,8 @@
override render() {
const chipClass = `summaryChip font-small ${this.styleType}`;
const grIcon = this.icon ? `gr-icons:${this.icon}` : '';
- return html`<button class="${chipClass}" @click="${this.handleClick}">
- ${this.icon && html`<iron-icon icon="${grIcon}"></iron-icon>`}
+ return html`<button class=${chipClass} @click=${this.handleClick}>
+ ${this.icon && html`<iron-icon icon=${grIcon}></iron-icon>`}
<slot></slot>
</button>`;
}
@@ -342,8 +342,8 @@
private renderChip(clazz: string, ariaLabel: string, icon: string) {
return html`
- <div class="${clazz}" role="link" tabindex="0" aria-label="${ariaLabel}">
- <iron-icon icon="${icon}"></iron-icon>
+ <div class=${clazz} role="link" tabindex="0" aria-label=${ariaLabel}>
+ <iron-icon icon=${icon}></iron-icon>
${this.renderLinks()}
<div class="text">${this.text}</div>
</div>
@@ -354,10 +354,10 @@
return this.links.map(
link => html`
<a
- href="${link}"
+ href=${link}
target="_blank"
- @click="${this.onLinkClick}"
- @keydown="${this.onLinkKeyDown}"
+ @click=${this.onLinkClick}
+ @keydown=${this.onLinkKeyDown}
aria-label="Link to check details"
><iron-icon class="launch" icon="gr-icons:launch"></iron-icon
></a>
@@ -614,7 +614,7 @@
if (!action) return;
return html`<gr-checks-action
context="summary"
- .action="${action}"
+ .action=${action}
></gr-checks-action>`;
}
@@ -634,9 +634,9 @@
link=""
vertical-offset="32"
horizontal-align="right"
- @tap-item="${this.handleAction}"
- .items="${items}"
- .disabledIds="${disabledIds}"
+ @tap-item=${this.handleAction}
+ .items=${items}
+ .disabledIds=${disabledIds}
>
<iron-icon icon="gr-icons:more-vert" aria-labelledby="moreMessage">
</iron-icon>
@@ -654,7 +654,7 @@
<iron-icon icon="gr-icons:error"></iron-icon>
</div>
<div class="right">
- <div class="message" title="${message}">
+ <div class="message" title=${message}>
Error while fetching results for ${plugin}: ${message}
</div>
</div>
@@ -675,7 +675,7 @@
Not logged in
</div>
<div class="right">
- <gr-button @click="${this.loginCallback}" link>Sign in</gr-button>
+ <gr-button @click=${this.loginCallback} link>Sign in</gr-button>
</div>
</div>
`;
@@ -736,10 +736,10 @@
if (count === 0) return;
const handler = () => this.onChipClick({statusOrCategory});
return html`<gr-checks-chip
- .statusOrCategory="${statusOrCategory}"
- .text="${`${count}`}"
- @click="${handler}"
- @keydown="${(e: KeyboardEvent) => handleSpaceOrEnter(e, handler)}"
+ .statusOrCategory=${statusOrCategory}
+ .text=${`${count}`}
+ @click=${handler}
+ @keydown=${(e: KeyboardEvent) => handleSpaceOrEnter(e, handler)}
></gr-checks-chip>`;
}
@@ -754,10 +754,10 @@
this.requestUpdate();
};
return html`<gr-checks-chip
- .statusOrCategory="${statusOrCategory}"
+ .statusOrCategory=${statusOrCategory}
.text="+ ${count} more"
- @click="${handler}"
- @keydown="${(e: KeyboardEvent) => handleSpaceOrEnter(e, handler)}"
+ @click=${handler}
+ @keydown=${(e: KeyboardEvent) => handleSpaceOrEnter(e, handler)}
></gr-checks-chip>`;
}
@@ -782,11 +782,11 @@
}
const handler = () => this.onChipClick(tabState);
return html`<gr-checks-chip
- .statusOrCategory="${statusOrCategory}"
- .text="${text}"
- .links="${links}"
- @click="${handler}"
- @keydown="${(e: KeyboardEvent) => handleSpaceOrEnter(e, handler)}"
+ .statusOrCategory=${statusOrCategory}
+ .text=${text}
+ .links=${links}
+ @click=${handler}
+ @keydown=${(e: KeyboardEvent) => handleSpaceOrEnter(e, handler)}
></gr-checks-chip>`;
}
@@ -834,7 +834,7 @@
: ''}${this.renderChecksChipRunning()}
<span
class="loadingSpin"
- ?hidden="${!this.someProvidersAreLoading}"
+ ?hidden=${!this.someProvidersAreLoading}
></span>
${this.renderErrorMessages()} ${this.renderChecksLogin()}
${this.renderSummaryMessage()} ${this.renderActions()}
@@ -866,7 +866,7 @@
${unresolvedAuthors.map(
account =>
html`<gr-avatar
- .account="${account}"
+ .account=${account}
imageSize="32"
></gr-avatar>`
)}
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 456ecf0..2dc401f 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
@@ -168,6 +168,7 @@
SwitchTabEvent,
SwitchTabEventDetail,
TabState,
+ ValueChangedEvent,
} from '../../../types/events';
import {GrButton} from '../../shared/gr-button/gr-button';
import {GrMessagesList} from '../gr-messages-list/gr-messages-list';
@@ -439,10 +440,6 @@
})
_changeStatuses?: ChangeStates[];
- /** If false, then the "Show more" button was used to expand. */
- @property({type: Boolean})
- _commitCollapsed = true;
-
/** Is the "Show more/less" button visible? */
@property({
type: Boolean,
@@ -934,6 +931,14 @@
});
}
+ handleEditingChanged(e: ValueChangedEvent<boolean>) {
+ this._editingCommitMessage = e.detail.value;
+ }
+
+ handleContentChanged(e: ValueChangedEvent) {
+ this._latestCommitMessage = e.detail.value;
+ }
+
_handleCommitMessageSave(e: EditableContentSaveEvent) {
assertIsDefined(this._change, '_change');
if (!this._changeNum)
diff --git a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_html.ts b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_html.ts
index b78fca0..72021e5 100644
--- a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_html.ts
+++ b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_html.ts
@@ -420,8 +420,10 @@
<div id="commitMessage" class="commitMessage">
<gr-editable-content
id="commitMessageEditor"
- editing="{{_editingCommitMessage}}"
- content="{{_latestCommitMessage}}"
+ editing="[[_editingCommitMessage]]"
+ content="[[_latestCommitMessage]]"
+ on-editing-changed="handleEditingChanged"
+ on-content-changed="handleContentChanged"
storage-key="[[_computeCommitMessageKey(_change._number, _change.current_revision)]]"
hide-edit-commit-message="[[_hideEditCommitMessage]]"
commit-collapsible="[[_commitCollapsible]]"
diff --git a/polygerrit-ui/app/elements/change/gr-commit-info/gr-commit-info.ts b/polygerrit-ui/app/elements/change/gr-commit-info/gr-commit-info.ts
index 0ce3b07..100b88e 100644
--- a/polygerrit-ui/app/elements/change/gr-commit-info/gr-commit-info.ts
+++ b/polygerrit-ui/app/elements/change/gr-commit-info/gr-commit-info.ts
@@ -60,12 +60,12 @@
<a
target="_blank"
rel="noopener"
- href="${this.computeCommitLink(
+ href=${this.computeCommitLink(
this._webLink,
this.change,
this.commitInfo,
this.serverConfig
- )}"
+ )}
>${this._computeShortHash(
this.change,
this.commitInfo,
@@ -74,9 +74,9 @@
>
<gr-copy-clipboard
hastooltip
- .buttonTitle="${'Copy full SHA to clipboard'}"
+ .buttonTitle=${'Copy full SHA to clipboard'}
hideinput
- .text="${this.commitInfo?.commit}"
+ .text=${this.commitInfo?.commit}
>
</gr-copy-clipboard>
</div>`;
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-conflict-dialog/gr-confirm-cherrypick-conflict-dialog.ts b/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-conflict-dialog/gr-confirm-cherrypick-conflict-dialog.ts
index 75b6053..5dd0ce7 100644
--- a/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-conflict-dialog/gr-confirm-cherrypick-conflict-dialog.ts
+++ b/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-conflict-dialog/gr-confirm-cherrypick-conflict-dialog.ts
@@ -14,24 +14,13 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-import '../../../styles/shared-styles';
+import {css, html, LitElement} from 'lit';
+import {customElement} from 'lit/decorators';
+import {sharedStyles} from '../../../styles/shared-styles';
import '../../shared/gr-dialog/gr-dialog';
-import {PolymerElement} from '@polymer/polymer/polymer-element';
-import {htmlTemplate} from './gr-confirm-cherrypick-conflict-dialog_html';
-import {customElement} from '@polymer/decorators';
-
-declare global {
- interface HTMLElementTagNameMap {
- 'gr-confirm-cherrypick-conflict-dialog': GrConfirmCherrypickConflictDialog;
- }
-}
@customElement('gr-confirm-cherrypick-conflict-dialog')
-export class GrConfirmCherrypickConflictDialog extends PolymerElement {
- static get template() {
- return htmlTemplate;
- }
-
+export class GrConfirmCherrypickConflictDialog extends LitElement {
/**
* Fired when the confirm button is pressed.
*
@@ -44,7 +33,44 @@
* @event cancel
*/
- _handleConfirmTap(e: Event) {
+ static override styles = [
+ sharedStyles,
+ css`
+ :host {
+ display: block;
+ }
+ :host([disabled]) {
+ opacity: 0.5;
+ pointer-events: none;
+ }
+ .main {
+ display: flex;
+ flex-direction: column;
+ width: 100%;
+ }
+ `,
+ ];
+
+ override render() {
+ return html`
+ <gr-dialog
+ confirm-label="Continue"
+ @confirm=${this.handleConfirmTap}
+ @cancel=${this.handleCancelTap}
+ >
+ <div class="header" slot="header">Cherry Pick Conflict!</div>
+ <div class="main" slot="main">
+ <span>Cherry Pick failed! (merge conflicts)</span>
+ <span
+ >Please select "Continue" to continue with conflicts or select
+ "cancel" to close the dialog.</span
+ >
+ </div>
+ </gr-dialog>
+ `;
+ }
+
+ handleConfirmTap(e: Event) {
e.preventDefault();
e.stopPropagation();
this.dispatchEvent(
@@ -55,7 +81,7 @@
);
}
- _handleCancelTap(e: Event) {
+ handleCancelTap(e: Event) {
e.preventDefault();
e.stopPropagation();
this.dispatchEvent(
@@ -66,3 +92,9 @@
);
}
}
+
+declare global {
+ interface HTMLElementTagNameMap {
+ 'gr-confirm-cherrypick-conflict-dialog': GrConfirmCherrypickConflictDialog;
+ }
+}
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-conflict-dialog/gr-confirm-cherrypick-conflict-dialog_html.ts b/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-conflict-dialog/gr-confirm-cherrypick-conflict-dialog_html.ts
deleted file mode 100644
index 5cf56b5..0000000
--- a/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-conflict-dialog/gr-confirm-cherrypick-conflict-dialog_html.ts
+++ /dev/null
@@ -1,49 +0,0 @@
-/**
- * @license
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-import {html} from '@polymer/polymer/lib/utils/html-tag';
-
-export const htmlTemplate = html`
- <style include="shared-styles">
- :host {
- display: block;
- }
- :host([disabled]) {
- opacity: 0.5;
- pointer-events: none;
- }
- .main {
- display: flex;
- flex-direction: column;
- width: 100%;
- }
- </style>
- <gr-dialog
- confirm-label="Continue"
- on-confirm="_handleConfirmTap"
- on-cancel="_handleCancelTap"
- >
- <div class="header" slot="header">Cherry Pick Conflict!</div>
- <div class="main" slot="main">
- <span>Cherry Pick failed! (merge conflicts)</span>
-
- <span
- >Please select "Continue" to continue with conflicts or select "cancel"
- to close the dialog.</span
- >
- </div>
- </gr-dialog>
-`;
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-conflict-dialog/gr-confirm-cherrypick-conflict-dialog_test.ts b/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-conflict-dialog/gr-confirm-cherrypick-conflict-dialog_test.ts
index f811619..ad89521 100644
--- a/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-conflict-dialog/gr-confirm-cherrypick-conflict-dialog_test.ts
+++ b/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-conflict-dialog/gr-confirm-cherrypick-conflict-dialog_test.ts
@@ -15,42 +15,60 @@
* limitations under the License.
*/
+import {fixture, html} from '@open-wc/testing-helpers';
import '../../../test/common-test-setup-karma';
import {queryAndAssert} from '../../../utils/common-util';
-import {fireEvent} from '../../../utils/event-util';
import './gr-confirm-cherrypick-conflict-dialog';
+import {GrDialog} from '../../shared/gr-dialog/gr-dialog';
import {GrConfirmCherrypickConflictDialog} from './gr-confirm-cherrypick-conflict-dialog';
-
-const basicFixture = fixtureFromElement(
- 'gr-confirm-cherrypick-conflict-dialog'
-);
+import {GrButton} from '../../shared/gr-button/gr-button';
suite('gr-confirm-cherrypick-conflict-dialog tests', () => {
let element: GrConfirmCherrypickConflictDialog;
- setup(() => {
- element = basicFixture.instantiate();
+ setup(async () => {
+ element = await fixture(
+ html`<gr-confirm-cherrypick-conflict-dialog></gr-confirm-cherrypick-conflict-dialog>`
+ );
});
- test('_handleConfirmTap', () => {
+ test('render', async () => {
+ expect(element).shadowDom.to.equal(/* HTML */ `
+ <gr-dialog confirm-label="Continue" role="dialog">
+ <div class="header" slot="header">Cherry Pick Conflict!</div>
+ <div class="main" slot="main">
+ <span>Cherry Pick failed! (merge conflicts)</span>
+ <span
+ >Please select "Continue" to continue with conflicts or select
+ "cancel" to close the dialog.</span
+ >
+ </div>
+ </gr-dialog>
+ `);
+ });
+
+ test('confirm', async () => {
const confirmHandler = sinon.stub();
element.addEventListener('confirm', confirmHandler);
- const confirmTapStub = sinon.spy(element, '_handleConfirmTap');
- fireEvent(queryAndAssert(element, 'gr-dialog'), 'confirm');
+
+ queryAndAssert<GrDialog>(element, 'gr-dialog').confirmButton!.click();
+ await element.updateComplete;
+
assert.isTrue(confirmHandler.called);
assert.isTrue(confirmHandler.calledOnce);
- assert.isTrue(confirmTapStub.called);
- assert.isTrue(confirmTapStub.calledOnce);
});
- test('_handleCancelTap', () => {
+ test('cancel', async () => {
const cancelHandler = sinon.stub();
element.addEventListener('cancel', cancelHandler);
- const cancelTapStub = sinon.spy(element, '_handleCancelTap');
- fireEvent(queryAndAssert(element, 'gr-dialog'), 'cancel');
+
+ queryAndAssert<GrButton>(
+ queryAndAssert<GrDialog>(element, 'gr-dialog'),
+ 'gr-button#cancel'
+ )!.click();
+ await element.updateComplete;
+
assert.isTrue(cancelHandler.called);
assert.isTrue(cancelHandler.calledOnce);
- assert.isTrue(cancelTapStub.called);
- assert.isTrue(cancelTapStub.calledOnce);
});
});
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-move-dialog/gr-confirm-move-dialog.ts b/polygerrit-ui/app/elements/change/gr-confirm-move-dialog/gr-confirm-move-dialog.ts
index c34c577..482efd6 100644
--- a/polygerrit-ui/app/elements/change/gr-confirm-move-dialog/gr-confirm-move-dialog.ts
+++ b/polygerrit-ui/app/elements/change/gr-confirm-move-dialog/gr-confirm-move-dialog.ts
@@ -14,33 +14,20 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-import '../../../styles/shared-styles';
-import '../../shared/gr-autocomplete/gr-autocomplete';
-import '../../shared/gr-dialog/gr-dialog';
-import {PolymerElement} from '@polymer/polymer/polymer-element';
-import {htmlTemplate} from './gr-confirm-move-dialog_html';
-import {customElement, property} from '@polymer/decorators';
+import {css, html, LitElement} from 'lit';
+import {customElement, property} from 'lit/decorators';
+import {sharedStyles} from '../../../styles/shared-styles';
import {BranchName, RepoName} from '../../../types/common';
import {getAppContext} from '../../../services/app-context';
-import {GrTypedAutocomplete} from '../../shared/gr-autocomplete/gr-autocomplete';
+import '../../shared/gr-autocomplete/gr-autocomplete';
+import '../../shared/gr-dialog/gr-dialog';
+import '@polymer/iron-autogrow-textarea/iron-autogrow-textarea';
import {addShortcut, Key, Modifier} from '../../../utils/dom-util';
const SUGGESTIONS_LIMIT = 15;
-// This is used to make sure 'branch'
-// can be typed as BranchName.
-export interface GrConfirmMoveDialog {
- $: {
- branchInput: GrTypedAutocomplete<BranchName>;
- };
-}
-
@customElement('gr-confirm-move-dialog')
-export class GrConfirmMoveDialog extends PolymerElement {
- static get template() {
- return htmlTemplate;
- }
-
+export class GrConfirmMoveDialog extends LitElement {
/**
* Fired when the confirm button is pressed.
*
@@ -62,9 +49,6 @@
@property({type: String})
project?: RepoName;
- @property({type: Object})
- _query?: (input: string) => Promise<{name: BranchName}[]>;
-
/** Called in disconnectedCallback. */
private cleanups: (() => void)[] = [];
@@ -78,24 +62,88 @@
super.connectedCallback();
this.cleanups.push(
addShortcut(this, {key: Key.ENTER, modifiers: [Modifier.CTRL_KEY]}, e =>
- this._handleConfirmTap(e)
+ this.handleConfirmTap(e)
)
);
this.cleanups.push(
addShortcut(this, {key: Key.ENTER, modifiers: [Modifier.META_KEY]}, e =>
- this._handleConfirmTap(e)
+ this.handleConfirmTap(e)
)
);
}
private readonly restApiService = getAppContext().restApiService;
- constructor() {
- super();
- this._query = (text: string) => this._getProjectBranchesSuggestions(text);
+ static override get styles() {
+ return [
+ sharedStyles,
+ css`
+ :host {
+ display: block;
+ width: 30em;
+ }
+ :host([disabled]) {
+ opacity: 0.5;
+ pointer-events: none;
+ }
+ label {
+ cursor: pointer;
+ }
+ .main {
+ display: flex;
+ flex-direction: column;
+ width: 100%;
+ }
+ .main label,
+ .main input[type='text'] {
+ display: block;
+ width: 100%;
+ }
+ .main .message {
+ width: 100%;
+ }
+ .warning {
+ color: var(--error-text-color);
+ }
+ `,
+ ];
}
- _handleConfirmTap(e: Event) {
+ override render() {
+ return html`
+ <gr-dialog
+ confirm-label="Move Change"
+ @confirm=${(e: Event) => this.handleConfirmTap(e)}
+ @cancel=${(e: Event) => this.handleCancelTap(e)}
+ >
+ <div class="header" slot="header">Move Change to Another Branch</div>
+ <div class="main" slot="main">
+ <p class="warning">
+ Warning: moving a change will not change its parents.
+ </p>
+ <label for="branchInput"> Move change to branch </label>
+ <gr-autocomplete
+ id="branchInput"
+ .text=${this.branch}
+ .query=${(text: string) => this.getProjectBranchesSuggestions(text)}
+ placeholder="Destination branch"
+ >
+ </gr-autocomplete>
+ <label for="messageInput"> Move Change Message </label>
+ <iron-autogrow-textarea
+ id="messageInput"
+ class="message"
+ autocomplete="on"
+ .rows=${4}
+ .maxRows=${15}
+ .bindValue=${this.message}
+ ></iron-autogrow-textarea>
+ </div>
+ </gr-dialog>
+ `;
+ }
+
+ private handleConfirmTap(e: Event) {
e.preventDefault();
e.stopPropagation();
this.dispatchEvent(
@@ -106,7 +154,7 @@
);
}
- _handleCancelTap(e: Event) {
+ private handleCancelTap(e: Event) {
e.preventDefault();
e.stopPropagation();
this.dispatchEvent(
@@ -117,7 +165,7 @@
);
}
- _getProjectBranchesSuggestions(input: string) {
+ private getProjectBranchesSuggestions(input: string) {
if (!this.project) return Promise.reject(new Error('Missing project'));
if (input.startsWith('refs/heads/')) {
input = input.substring('refs/heads/'.length);
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-move-dialog/gr-confirm-move-dialog_html.ts b/polygerrit-ui/app/elements/change/gr-confirm-move-dialog/gr-confirm-move-dialog_html.ts
deleted file mode 100644
index 7c3c719..0000000
--- a/polygerrit-ui/app/elements/change/gr-confirm-move-dialog/gr-confirm-move-dialog_html.ts
+++ /dev/null
@@ -1,78 +0,0 @@
-/**
- * @license
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-import {html} from '@polymer/polymer/lib/utils/html-tag';
-
-export const htmlTemplate = html`
- <style include="shared-styles">
- :host {
- display: block;
- width: 30em;
- }
- :host([disabled]) {
- opacity: 0.5;
- pointer-events: none;
- }
- label {
- cursor: pointer;
- }
- .main {
- display: flex;
- flex-direction: column;
- width: 100%;
- }
- .main label,
- .main input[type='text'] {
- display: block;
- width: 100%;
- }
- .main .message {
- width: 100%;
- }
- .warning {
- color: var(--error-text-color);
- }
- </style>
- <gr-dialog
- confirm-label="Move Change"
- on-confirm="_handleConfirmTap"
- on-cancel="_handleCancelTap"
- >
- <div class="header" slot="header">Move Change to Another Branch</div>
- <div class="main" slot="main">
- <p class="warning">
- Warning: moving a change will not change its parents.
- </p>
- <label for="branchInput"> Move change to branch </label>
- <gr-autocomplete
- id="branchInput"
- text="{{branch}}"
- query="[[_query]]"
- placeholder="Destination branch"
- >
- </gr-autocomplete>
- <label for="messageInput"> Move Change Message </label>
- <iron-autogrow-textarea
- id="messageInput"
- class="message"
- autocomplete="on"
- rows="4"
- max-rows="15"
- bind-value="{{message}}"
- ></iron-autogrow-textarea>
- </div>
- </gr-dialog>
-`;
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-move-dialog/gr-confirm-move-dialog_test.ts b/polygerrit-ui/app/elements/change/gr-confirm-move-dialog/gr-confirm-move-dialog_test.ts
index ea5d320..af75b48 100644
--- a/polygerrit-ui/app/elements/change/gr-confirm-move-dialog/gr-confirm-move-dialog_test.ts
+++ b/polygerrit-ui/app/elements/change/gr-confirm-move-dialog/gr-confirm-move-dialog_test.ts
@@ -18,15 +18,15 @@
import '../../../test/common-test-setup-karma';
import './gr-confirm-move-dialog';
import {GrConfirmMoveDialog} from './gr-confirm-move-dialog';
-import {stubRestApi} from '../../../test/test-utils';
+import {queryAndAssert, stubRestApi} from '../../../test/test-utils';
import {BranchName, GitRef, RepoName} from '../../../types/common';
-
-const basicFixture = fixtureFromElement('gr-confirm-move-dialog');
+import {fixture, html} from '@open-wc/testing-helpers';
+import {GrAutocomplete} from '../../shared/gr-autocomplete/gr-autocomplete';
suite('gr-confirm-move-dialog tests', () => {
let element: GrConfirmMoveDialog;
- setup(() => {
+ setup(async () => {
stubRestApi('getRepoBranches').callsFake((input: string) => {
if (input.startsWith('test')) {
return Promise.resolve([
@@ -40,35 +40,70 @@
return Promise.resolve([]);
}
});
- element = basicFixture.instantiate();
- element.project = 'test-repo' as RepoName;
+ element = await fixture(
+ html`<gr-confirm-move-dialog
+ .project=${'test-repo' as RepoName}
+ ></gr-confirm-move-dialog>`
+ );
});
- test('with updated commit message', () => {
+ test('render', async () => {
+ expect(element).shadowDom.to.equal(/* HTML */ `
+ <gr-dialog confirm-label="Move Change" role="dialog">
+ <div class="header" slot="header">Move Change to Another Branch</div>
+ <div class="main" slot="main">
+ <p class="warning">
+ Warning: moving a change will not change its parents.
+ </p>
+ <label for="branchInput"> Move change to branch </label>
+ <gr-autocomplete id="branchInput" placeholder="Destination branch">
+ </gr-autocomplete>
+ <label for="messageInput"> Move Change Message </label>
+ <iron-autogrow-textarea
+ aria-disabled="false"
+ id="messageInput"
+ class="message"
+ autocomplete="on"
+ ></iron-autogrow-textarea>
+ </div>
+ </gr-dialog>
+ `);
+ });
+
+ test('with updated commit message', async () => {
element.branch = 'master' as BranchName;
const myNewMessage = 'updated commit message';
element.message = myNewMessage;
- flush();
+ await element.updateComplete;
+
assert.equal(element.message, myNewMessage);
});
- test('_getProjectBranchesSuggestions empty', async () => {
- const branches = await element._getProjectBranchesSuggestions(
- 'nonexistent'
+ test('suggestions empty', async () => {
+ const autoComplete = queryAndAssert<GrAutocomplete>(
+ element,
+ 'gr-autocomplete'
);
+ const branches = await autoComplete.query!('nonexistent');
assert.equal(branches.length, 0);
});
- test('_getProjectBranchesSuggestions non-empty', async () => {
- const branches = await element._getProjectBranchesSuggestions(
- 'test-branch'
+ test('suggestions non-empty', async () => {
+ const autoComplete = queryAndAssert<GrAutocomplete>(
+ element,
+ 'gr-autocomplete'
);
+ const branches = await autoComplete.query!('test-branch');
assert.equal(branches.length, 1);
assert.equal(branches[0].name, 'test-branch');
});
- test('_getProjectBranchesSuggestions input empty string', async () => {
- const branches = await element._getProjectBranchesSuggestions('');
+ test('suggestions input empty string', async () => {
+ const autoComplete = queryAndAssert<GrAutocomplete>(
+ element,
+ 'gr-autocomplete'
+ );
+ const branches = await autoComplete.query!('');
assert.equal(branches.length, 0);
});
});
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 00f65b0..193e136 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
@@ -14,20 +14,18 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-import '../../shared/gr-autocomplete/gr-autocomplete';
-import '../../shared/gr-dialog/gr-dialog';
-import '../../../styles/shared-styles';
-import {PolymerElement} from '@polymer/polymer/polymer-element';
-import {htmlTemplate} from './gr-confirm-rebase-dialog_html';
-import {customElement, property, observe} from '@polymer/decorators';
+import {css, html, LitElement, PropertyValues} from 'lit';
+import {customElement, property, query, state} from 'lit/decorators';
import {NumericChangeId, BranchName} from '../../../types/common';
+import '../../shared/gr-dialog/gr-dialog';
+import '../../shared/gr-autocomplete/gr-autocomplete';
import {
GrAutocomplete,
AutocompleteQuery,
AutocompleteSuggestion,
} from '../../shared/gr-autocomplete/gr-autocomplete';
import {getAppContext} from '../../../services/app-context';
-import {GrDialog} from '../../shared/gr-dialog/gr-dialog';
+import {sharedStyles} from '../../../styles/shared-styles';
export interface RebaseChange {
name: string;
@@ -38,26 +36,8 @@
base: string | null;
}
-export interface GrConfirmRebaseDialog {
- $: {
- confirmDialog: GrDialog;
- parentInput: GrAutocomplete;
- parentUpToDateMsg: HTMLDivElement;
- rebaseOnParent: HTMLDivElement;
- rebaseOnParentInput: HTMLInputElement;
- rebaseOnOtherInput: HTMLInputElement;
- rebaseOnTip: HTMLDivElement;
- rebaseOnTipInput: HTMLInputElement;
- tipUpToDateMsg: HTMLDivElement;
- };
-}
-
@customElement('gr-confirm-rebase-dialog')
-export class GrConfirmRebaseDialog extends PolymerElement {
- static get template() {
- return htmlTemplate;
- }
-
+export class GrConfirmRebaseDialog extends LitElement {
/**
* Fired when the confirm button is pressed.
*
@@ -82,20 +62,154 @@
@property({type: Boolean})
rebaseOnCurrent?: boolean;
- @property({type: String})
- _text = '';
+ @state()
+ text = '';
- @property({type: Object})
- _query: AutocompleteQuery = () => Promise.resolve([]);
+ @state()
+ private query: AutocompleteQuery;
- @property({type: Array})
- _recentChanges?: RebaseChange[];
+ @state()
+ recentChanges?: RebaseChange[];
+
+ @query('#rebaseOnParentInput')
+ private rebaseOnParentInput!: HTMLInputElement;
+
+ @query('#rebaseOnTipInput')
+ private rebaseOnTipInput!: HTMLInputElement;
+
+ @query('#rebaseOnOtherInput')
+ rebaseOnOtherInput!: HTMLInputElement;
+
+ @query('#parentInput')
+ parentInput!: GrAutocomplete;
private readonly restApiService = getAppContext().restApiService;
constructor() {
super();
- this._query = input => this._getChangeSuggestions(input);
+ this.query = input => this.getChangeSuggestions(input);
+ }
+
+ override willUpdate(changedProperties: PropertyValues): void {
+ if (
+ changedProperties.has('rebaseOnCurrent') ||
+ changedProperties.has('hasParent')
+ ) {
+ this.updateSelectedOption();
+ }
+ }
+
+ static override styles = [
+ sharedStyles,
+ css`
+ :host {
+ display: block;
+ width: 30em;
+ }
+ :host([disabled]) {
+ opacity: 0.5;
+ pointer-events: none;
+ }
+ label {
+ cursor: pointer;
+ }
+ .message {
+ font-style: italic;
+ }
+ .parentRevisionContainer label,
+ .parentRevisionContainer input[type='text'] {
+ display: block;
+ width: 100%;
+ }
+ .parentRevisionContainer label {
+ margin-bottom: var(--spacing-xs);
+ }
+ .rebaseOption {
+ margin: var(--spacing-m) 0;
+ }
+ `,
+ ];
+
+ override render() {
+ return html`
+ <gr-dialog
+ id="confirmDialog"
+ confirm-label="Rebase"
+ @confirm=${this.handleConfirmTap}
+ @cancel=${this.handleCancelTap}
+ >
+ <div class="header" slot="header">Confirm rebase</div>
+ <div class="main" slot="main">
+ <div
+ id="rebaseOnParent"
+ class="rebaseOption"
+ ?hidden=${!this.displayParentOption()}
+ >
+ <input id="rebaseOnParentInput" name="rebaseOptions" type="radio" />
+ <label id="rebaseOnParentLabel" for="rebaseOnParentInput">
+ Rebase on parent change
+ </label>
+ </div>
+ <div
+ id="parentUpToDateMsg"
+ class="message"
+ ?hidden=${!this.displayParentUpToDateMsg()}
+ >
+ This change is up to date with its parent.
+ </div>
+ <div
+ id="rebaseOnTip"
+ class="rebaseOption"
+ ?hidden=${!this.displayTipOption()}
+ >
+ <input
+ id="rebaseOnTipInput"
+ name="rebaseOptions"
+ type="radio"
+ ?disabled=${!this.displayTipOption()}
+ />
+ <label id="rebaseOnTipLabel" for="rebaseOnTipInput">
+ Rebase on top of the ${this.branch} branch<span
+ ?hidden=${!this.hasParent}
+ >
+ (breaks relation chain)
+ </span>
+ </label>
+ </div>
+ <div
+ id="tipUpToDateMsg"
+ class="message"
+ ?hidden=${this.displayTipOption()}
+ >
+ Change is up to date with the target branch already (${this.branch})
+ </div>
+ <div id="rebaseOnOther" class="rebaseOption">
+ <input
+ id="rebaseOnOtherInput"
+ name="rebaseOptions"
+ type="radio"
+ @click=${this.handleRebaseOnOther}
+ />
+ <label id="rebaseOnOtherLabel" for="rebaseOnOtherInput">
+ Rebase on a specific change, ref, or commit
+ <span ?hidden=${!this.hasParent}> (breaks relation chain) </span>
+ </label>
+ </div>
+ <div class="parentRevisionContainer">
+ <gr-autocomplete
+ id="parentInput"
+ .query=${this.query}
+ no-debounce
+ text=${this.text}
+ @click=${this.handleEnterChangeNumberClick}
+ allow-non-suggested-values
+ placeholder="Change number, ref, or commit hash"
+ >
+ </gr-autocomplete>
+ </div>
+ </div>
+ </gr-dialog>
+ `;
}
// This is called by gr-change-actions every time the rebase dialog is
@@ -116,25 +230,25 @@
value: change._number,
});
}
- this._recentChanges = changes;
- return this._recentChanges;
+ this.recentChanges = changes;
+ return this.recentChanges;
});
}
- _getRecentChanges() {
- if (this._recentChanges) {
- return Promise.resolve(this._recentChanges);
+ getRecentChanges() {
+ if (this.recentChanges) {
+ return Promise.resolve(this.recentChanges);
}
return this.fetchRecentChanges();
}
- _getChangeSuggestions(input: string) {
- return this._getRecentChanges().then(changes =>
- this._filterChanges(input, changes)
+ private getChangeSuggestions(input: string) {
+ return this.getRecentChanges().then(changes =>
+ this.filterChanges(input, changes)
);
}
- _filterChanges(
+ filterChanges(
input: string,
changes: RebaseChange[]
): AutocompleteSuggestion[] {
@@ -152,16 +266,16 @@
);
}
- _displayParentOption(rebaseOnCurrent?: boolean, hasParent?: boolean) {
- return hasParent && rebaseOnCurrent;
+ private displayParentOption() {
+ return this.hasParent && this.rebaseOnCurrent;
}
- _displayParentUpToDateMsg(rebaseOnCurrent?: boolean, hasParent?: boolean) {
- return hasParent && !rebaseOnCurrent;
+ private displayParentUpToDateMsg() {
+ return this.hasParent && !this.rebaseOnCurrent;
}
- _displayTipOption(rebaseOnCurrent?: boolean, hasParent?: boolean) {
- return !(!rebaseOnCurrent && !hasParent);
+ private displayTipOption() {
+ return this.rebaseOnCurrent || this.hasParent;
}
/**
@@ -171,63 +285,62 @@
* rebased on top of the target branch. Leaving out the base implies that it
* should be rebased on top of its current parent.
*/
- _getSelectedBase() {
- if (this.$.rebaseOnParentInput.checked) {
+ getSelectedBase() {
+ if (this.rebaseOnParentInput.checked) {
return null;
}
- if (this.$.rebaseOnTipInput.checked) {
+ if (this.rebaseOnTipInput.checked) {
return '';
}
- if (!this._text) {
+ if (!this.text) {
return '';
}
// Change numbers will have their description appended by the
// autocomplete.
- return this._text.split(':')[0];
+ return this.text.split(':')[0];
}
- _handleConfirmTap(e: Event) {
+ private handleConfirmTap(e: Event) {
e.preventDefault();
e.stopPropagation();
const detail: ConfirmRebaseEventDetail = {
- base: this._getSelectedBase(),
+ base: this.getSelectedBase(),
};
this.dispatchEvent(new CustomEvent('confirm', {detail}));
- this._text = '';
+ this.text = '';
}
- _handleCancelTap(e: Event) {
+ private handleCancelTap(e: Event) {
e.preventDefault();
e.stopPropagation();
this.dispatchEvent(new CustomEvent('cancel'));
- this._text = '';
+ this.text = '';
}
- _handleRebaseOnOther() {
- this.$.parentInput.focus();
+ private handleRebaseOnOther() {
+ this.parentInput.focus();
}
- _handleEnterChangeNumberClick() {
- this.$.rebaseOnOtherInput.checked = true;
+ private handleEnterChangeNumberClick() {
+ this.rebaseOnOtherInput.checked = true;
}
/**
* Sets the default radio button based on the state of the app and
* the corresponding value to be submitted.
*/
- @observe('rebaseOnCurrent', 'hasParent')
- _updateSelectedOption(rebaseOnCurrent?: boolean, hasParent?: boolean) {
- // Polymer 2: check for undefined
+ private updateSelectedOption() {
+ const {rebaseOnCurrent, hasParent} = this;
if (rebaseOnCurrent === undefined || hasParent === undefined) {
return;
}
- if (this._displayParentOption(rebaseOnCurrent, hasParent)) {
- this.$.rebaseOnParentInput.checked = true;
- } else if (this._displayTipOption(rebaseOnCurrent, hasParent)) {
- this.$.rebaseOnTipInput.checked = true;
+ if (this.displayParentOption()) {
+ this.rebaseOnParentInput.checked = true;
+ } else if (this.displayTipOption()) {
+ this.rebaseOnTipInput.checked = true;
} else {
- this.$.rebaseOnOtherInput.checked = true;
+ this.rebaseOnOtherInput.checked = true;
}
}
}
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-rebase-dialog/gr-confirm-rebase-dialog_html.ts b/polygerrit-ui/app/elements/change/gr-confirm-rebase-dialog/gr-confirm-rebase-dialog_html.ts
deleted file mode 100644
index 1052201..0000000
--- a/polygerrit-ui/app/elements/change/gr-confirm-rebase-dialog/gr-confirm-rebase-dialog_html.ts
+++ /dev/null
@@ -1,122 +0,0 @@
-/**
- * @license
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-import {html} from '@polymer/polymer/lib/utils/html-tag';
-
-export const htmlTemplate = html`
- <style include="shared-styles">
- :host {
- display: block;
- width: 30em;
- }
- :host([disabled]) {
- opacity: 0.5;
- pointer-events: none;
- }
- label {
- cursor: pointer;
- }
- .message {
- font-style: italic;
- }
- .parentRevisionContainer label,
- .parentRevisionContainer input[type='text'] {
- display: block;
- width: 100%;
- }
- .parentRevisionContainer label {
- margin-bottom: var(--spacing-xs);
- }
- .rebaseOption {
- margin: var(--spacing-m) 0;
- }
- </style>
- <gr-dialog
- id="confirmDialog"
- confirm-label="Rebase"
- on-confirm="_handleConfirmTap"
- on-cancel="_handleCancelTap"
- >
- <div class="header" slot="header">Confirm rebase</div>
- <div class="main" slot="main">
- <div
- id="rebaseOnParent"
- class="rebaseOption"
- hidden$="[[!_displayParentOption(rebaseOnCurrent, hasParent)]]"
- >
- <input id="rebaseOnParentInput" name="rebaseOptions" type="radio" />
- <label id="rebaseOnParentLabel" for="rebaseOnParentInput">
- Rebase on parent change
- </label>
- </div>
- <div
- id="parentUpToDateMsg"
- class="message"
- hidden$="[[!_displayParentUpToDateMsg(rebaseOnCurrent, hasParent)]]"
- >
- This change is up to date with its parent.
- </div>
- <div
- id="rebaseOnTip"
- class="rebaseOption"
- hidden$="[[!_displayTipOption(rebaseOnCurrent, hasParent)]]"
- >
- <input
- id="rebaseOnTipInput"
- name="rebaseOptions"
- type="radio"
- disabled$="[[!_displayTipOption(rebaseOnCurrent, hasParent)]]"
- />
- <label id="rebaseOnTipLabel" for="rebaseOnTipInput">
- Rebase on top of the [[branch]] branch<span hidden$="[[!hasParent]]">
- (breaks relation chain)
- </span>
- </label>
- </div>
- <div
- id="tipUpToDateMsg"
- class="message"
- hidden$="[[_displayTipOption(rebaseOnCurrent, hasParent)]]"
- >
- Change is up to date with the target branch already ([[branch]])
- </div>
- <div id="rebaseOnOther" class="rebaseOption">
- <input
- id="rebaseOnOtherInput"
- name="rebaseOptions"
- type="radio"
- on-click="_handleRebaseOnOther"
- />
- <label id="rebaseOnOtherLabel" for="rebaseOnOtherInput">
- Rebase on a specific change, ref, or commit
- <span hidden$="[[!hasParent]]"> (breaks relation chain) </span>
- </label>
- </div>
- <div class="parentRevisionContainer">
- <gr-autocomplete
- id="parentInput"
- query="[[_query]]"
- no-debounce=""
- text="{{_text}}"
- on-click="_handleEnterChangeNumberClick"
- allow-non-suggested-values=""
- placeholder="Change number, ref, or commit hash"
- >
- </gr-autocomplete>
- </div>
- </div>
- </gr-dialog>
-`;
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 74c1b3c..17afe92 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
@@ -18,95 +18,225 @@
import '../../../test/common-test-setup-karma';
import './gr-confirm-rebase-dialog';
import {GrConfirmRebaseDialog, RebaseChange} from './gr-confirm-rebase-dialog';
-import {stubRestApi} from '../../../test/test-utils';
+import {queryAndAssert, stubRestApi} from '../../../test/test-utils';
import * as MockInteractions from '@polymer/iron-test-helpers/mock-interactions';
import {NumericChangeId} from '../../../types/common';
import {createChangeViewChange} from '../../../test/test-data-generators';
-
-const basicFixture = fixtureFromElement('gr-confirm-rebase-dialog');
+import {fixture, html} from '@open-wc/testing-helpers';
suite('gr-confirm-rebase-dialog tests', () => {
let element: GrConfirmRebaseDialog;
- setup(() => {
- element = basicFixture.instantiate();
+ setup(async () => {
+ element = await fixture(
+ html`<gr-confirm-rebase-dialog></gr-confirm-rebase-dialog>`
+ );
});
- test('controls with parent and rebase on current available', () => {
+ test('render', async () => {
+ expect(element).shadowDom.to.equal(/* HTML*/ `
+ <gr-dialog
+ id="confirmDialog"
+ confirm-label="Rebase"
+ role="dialog"
+ >
+ <div class="header" slot="header">Confirm rebase</div>
+ <div class="main" slot="main">
+ <div
+ id="rebaseOnParent"
+ class="rebaseOption"
+ hidden=""
+ >
+ <input id="rebaseOnParentInput" name="rebaseOptions" type="radio" />
+ <label id="rebaseOnParentLabel" for="rebaseOnParentInput">
+ Rebase on parent change
+ </label>
+ </div>
+ <div
+ id="parentUpToDateMsg"
+ class="message"
+ hidden=""
+ >
+ This change is up to date with its parent.
+ </div>
+ <div
+ id="rebaseOnTip"
+ class="rebaseOption"
+ hidden=""
+ >
+ <input
+ disabled=""
+ id="rebaseOnTipInput"
+ name="rebaseOptions"
+ type="radio"
+ />
+ <label id="rebaseOnTipLabel" for="rebaseOnTipInput">
+ Rebase on top of the branch<span hidden=""
+ >
+ (breaks relation chain)
+ </span>
+ </label>
+ </div>
+ <div
+ id="tipUpToDateMsg"
+ class="message"
+ >
+ Change is up to date with the target branch already ()
+ </div>
+ <div id="rebaseOnOther" class="rebaseOption">
+ <input
+ id="rebaseOnOtherInput"
+ name="rebaseOptions"
+ type="radio"
+ />
+ <label id="rebaseOnOtherLabel" for="rebaseOnOtherInput">
+ Rebase on a specific change, ref, or commit
+ <span hidden=""> (breaks relation chain) </span>
+ </label>
+ </div>
+ <div class="parentRevisionContainer">
+ <gr-autocomplete
+ id="parentInput"
+ no-debounce=""
+ allow-non-suggested-values
+ placeholder="Change number, ref, or commit hash"
+ text=""
+ >
+ </gr-autocomplete>
+ </div>
+ </div>
+ </gr-dialog>
+ `);
+ });
+
+ test('controls with parent and rebase on current available', async () => {
element.rebaseOnCurrent = true;
element.hasParent = true;
- flush();
- assert.isTrue(element.$.rebaseOnParentInput.checked);
- assert.isFalse(element.$.rebaseOnParent.hasAttribute('hidden'));
- assert.isTrue(element.$.parentUpToDateMsg.hasAttribute('hidden'));
- assert.isFalse(element.$.rebaseOnTip.hasAttribute('hidden'));
- assert.isTrue(element.$.tipUpToDateMsg.hasAttribute('hidden'));
+ await element.updateComplete;
+
+ assert.isTrue(
+ queryAndAssert<HTMLInputElement>(element, '#rebaseOnParentInput').checked
+ );
+ assert.isFalse(
+ queryAndAssert(element, '#rebaseOnParent').hasAttribute('hidden')
+ );
+ assert.isTrue(
+ queryAndAssert(element, '#parentUpToDateMsg').hasAttribute('hidden')
+ );
+ assert.isFalse(
+ queryAndAssert(element, '#rebaseOnTip').hasAttribute('hidden')
+ );
+ assert.isTrue(
+ queryAndAssert(element, '#tipUpToDateMsg').hasAttribute('hidden')
+ );
});
- test('controls with parent rebase on current not available', () => {
+ test('controls with parent rebase on current not available', async () => {
element.rebaseOnCurrent = false;
element.hasParent = true;
- flush();
- assert.isTrue(element.$.rebaseOnTipInput.checked);
- assert.isTrue(element.$.rebaseOnParent.hasAttribute('hidden'));
- assert.isFalse(element.$.parentUpToDateMsg.hasAttribute('hidden'));
- assert.isFalse(element.$.rebaseOnTip.hasAttribute('hidden'));
- assert.isTrue(element.$.tipUpToDateMsg.hasAttribute('hidden'));
+ await element.updateComplete;
+
+ assert.isTrue(
+ queryAndAssert<HTMLInputElement>(element, '#rebaseOnTipInput').checked
+ );
+ assert.isTrue(
+ queryAndAssert(element, '#rebaseOnParent').hasAttribute('hidden')
+ );
+ assert.isFalse(
+ queryAndAssert(element, '#parentUpToDateMsg').hasAttribute('hidden')
+ );
+ assert.isFalse(
+ queryAndAssert(element, '#rebaseOnTip').hasAttribute('hidden')
+ );
+ assert.isTrue(
+ queryAndAssert(element, '#tipUpToDateMsg').hasAttribute('hidden')
+ );
});
- test('controls without parent and rebase on current available', () => {
+ test('controls without parent and rebase on current available', async () => {
element.rebaseOnCurrent = true;
element.hasParent = false;
- flush();
- assert.isTrue(element.$.rebaseOnTipInput.checked);
- assert.isTrue(element.$.rebaseOnParent.hasAttribute('hidden'));
- assert.isTrue(element.$.parentUpToDateMsg.hasAttribute('hidden'));
- assert.isFalse(element.$.rebaseOnTip.hasAttribute('hidden'));
- assert.isTrue(element.$.tipUpToDateMsg.hasAttribute('hidden'));
+ await element.updateComplete;
+
+ assert.isTrue(
+ queryAndAssert<HTMLInputElement>(element, '#rebaseOnTipInput').checked
+ );
+ assert.isTrue(
+ queryAndAssert(element, '#rebaseOnParent').hasAttribute('hidden')
+ );
+ assert.isTrue(
+ queryAndAssert(element, '#parentUpToDateMsg').hasAttribute('hidden')
+ );
+ assert.isFalse(
+ queryAndAssert(element, '#rebaseOnTip').hasAttribute('hidden')
+ );
+ assert.isTrue(
+ queryAndAssert(element, '#tipUpToDateMsg').hasAttribute('hidden')
+ );
});
- test('controls without parent rebase on current not available', () => {
+ test('controls without parent rebase on current not available', async () => {
element.rebaseOnCurrent = false;
element.hasParent = false;
- flush();
- assert.isTrue(element.$.rebaseOnOtherInput.checked);
- assert.isTrue(element.$.rebaseOnParent.hasAttribute('hidden'));
- assert.isTrue(element.$.parentUpToDateMsg.hasAttribute('hidden'));
- assert.isTrue(element.$.rebaseOnTip.hasAttribute('hidden'));
- assert.isFalse(element.$.tipUpToDateMsg.hasAttribute('hidden'));
+ await element.updateComplete;
+
+ assert.isTrue(element.rebaseOnOtherInput.checked);
+ assert.isTrue(
+ queryAndAssert(element, '#rebaseOnParent').hasAttribute('hidden')
+ );
+ assert.isTrue(
+ queryAndAssert(element, '#parentUpToDateMsg').hasAttribute('hidden')
+ );
+ assert.isTrue(
+ queryAndAssert(element, '#rebaseOnTip').hasAttribute('hidden')
+ );
+ assert.isFalse(
+ queryAndAssert(element, '#tipUpToDateMsg').hasAttribute('hidden')
+ );
});
- test('input cleared on cancel or submit', () => {
- element._text = '123';
- element.$.confirmDialog.dispatchEvent(
+ test('input cleared on cancel or submit', async () => {
+ element.text = '123';
+ await element.updateComplete;
+ queryAndAssert(element, '#confirmDialog').dispatchEvent(
new CustomEvent('confirm', {
composed: true,
bubbles: true,
})
);
- assert.equal(element._text, '');
+ assert.equal(element.text, '');
- element._text = '123';
- element.$.confirmDialog.dispatchEvent(
+ element.text = '123';
+ await element.updateComplete;
+
+ queryAndAssert(element, '#confirmDialog').dispatchEvent(
new CustomEvent('cancel', {
composed: true,
bubbles: true,
})
);
- assert.equal(element._text, '');
+ assert.equal(element.text, '');
});
- test('_getSelectedBase', () => {
- element._text = '5fab321c';
- element.$.rebaseOnParentInput.checked = true;
- assert.equal(element._getSelectedBase(), null);
- element.$.rebaseOnParentInput.checked = false;
- element.$.rebaseOnTipInput.checked = true;
- assert.equal(element._getSelectedBase(), '');
- element.$.rebaseOnTipInput.checked = false;
- assert.equal(element._getSelectedBase(), element._text);
- element._text = '101: Test';
- assert.equal(element._getSelectedBase(), '101');
+ test('_getSelectedBase', async () => {
+ element.text = '5fab321c';
+ await element.updateComplete;
+
+ queryAndAssert<HTMLInputElement>(element, '#rebaseOnParentInput').checked =
+ true;
+ assert.equal(element.getSelectedBase(), null);
+ queryAndAssert<HTMLInputElement>(element, '#rebaseOnParentInput').checked =
+ false;
+ queryAndAssert<HTMLInputElement>(element, '#rebaseOnTipInput').checked =
+ true;
+ assert.equal(element.getSelectedBase(), '');
+ queryAndAssert<HTMLInputElement>(element, '#rebaseOnTipInput').checked =
+ false;
+ assert.equal(element.getSelectedBase(), element.text);
+ element.text = '101: Test';
+ await element.updateComplete;
+
+ assert.equal(element.getSelectedBase(), '101');
});
suite('parent suggestions', () => {
@@ -149,46 +279,52 @@
);
});
- test('_getRecentChanges', () => {
- const recentChangesSpy = sinon.spy(element, '_getRecentChanges');
- return element
- ._getRecentChanges()
- .then(() => {
- assert.deepEqual(element._recentChanges, recentChanges);
- assert.equal(getChangesStub.callCount, 1);
- // When called a second time, should not re-request recent changes.
- element._getRecentChanges();
- })
- .then(() => {
- assert.equal(recentChangesSpy.callCount, 2);
- assert.equal(getChangesStub.callCount, 1);
- });
+ test('_getRecentChanges', async () => {
+ const recentChangesSpy = sinon.spy(element, 'getRecentChanges');
+ await element.getRecentChanges();
+ await element.updateComplete;
+
+ assert.deepEqual(element.recentChanges, recentChanges);
+ assert.equal(getChangesStub.callCount, 1);
+
+ // When called a second time, should not re-request recent changes.
+ await element.getRecentChanges();
+ await element.updateComplete;
+
+ assert.equal(recentChangesSpy.callCount, 2);
+ assert.equal(getChangesStub.callCount, 1);
});
- test('_filterChanges', () => {
- assert.equal(element._filterChanges('123', recentChanges).length, 1);
- assert.equal(element._filterChanges('12', recentChanges).length, 2);
- assert.equal(element._filterChanges('awesome', recentChanges).length, 3);
- assert.equal(element._filterChanges('third', recentChanges).length, 1);
+ test('_filterChanges', async () => {
+ assert.equal(element.filterChanges('123', recentChanges).length, 1);
+ assert.equal(element.filterChanges('12', recentChanges).length, 2);
+ assert.equal(element.filterChanges('awesome', recentChanges).length, 3);
+ assert.equal(element.filterChanges('third', recentChanges).length, 1);
element.changeNumber = 123 as NumericChangeId;
- assert.equal(element._filterChanges('123', recentChanges).length, 0);
- assert.equal(element._filterChanges('124', recentChanges).length, 1);
- assert.equal(element._filterChanges('awesome', recentChanges).length, 2);
+ await element.updateComplete;
+
+ assert.equal(element.filterChanges('123', recentChanges).length, 0);
+ assert.equal(element.filterChanges('124', recentChanges).length, 1);
+ assert.equal(element.filterChanges('awesome', recentChanges).length, 2);
});
- test('input text change triggers function', () => {
- const recentChangesSpy = sinon.spy(element, '_getRecentChanges');
- element.$.parentInput.noDebounce = true;
+ test('input text change triggers function', async () => {
+ const recentChangesSpy = sinon.spy(element, 'getRecentChanges');
+ element.parentInput.noDebounce = true;
MockInteractions.pressAndReleaseKeyOn(
- element.$.parentInput.$.input,
+ queryAndAssert(queryAndAssert(element, '#parentInput'), '#input'),
13,
null,
'enter'
);
- element._text = '1';
+ element.text = '1';
+ await element.updateComplete;
+
assert.isTrue(recentChangesSpy.calledOnce);
- element._text = '12';
+ element.text = '12';
+ await element.updateComplete;
+
assert.isTrue(recentChangesSpy.calledTwice);
});
});
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 e0532c2..8ea2bf5 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
@@ -15,14 +15,15 @@
* limitations under the License.
*/
import '../../shared/gr-dialog/gr-dialog';
-import '../../../styles/shared-styles';
import '../../plugins/gr-endpoint-decorator/gr-endpoint-decorator';
-import {PolymerElement} from '@polymer/polymer/polymer-element';
-import {htmlTemplate} from './gr-confirm-revert-dialog_html';
-import {customElement, property} from '@polymer/decorators';
+import '@polymer/iron-autogrow-textarea/iron-autogrow-textarea';
+import {LitElement, html, css, nothing} from 'lit';
+import {customElement, state} from 'lit/decorators';
import {ChangeInfo, CommitId} from '../../../types/common';
import {fireAlert} from '../../../utils/event-util';
import {getAppContext} from '../../../services/app-context';
+import {sharedStyles} from '../../../styles/shared-styles';
+import {BindValueChangeEvent} from '../../../types/events';
const ERR_COMMIT_NOT_FOUND = 'Unable to find the commit hash of this change.';
const CHANGE_SUBJECT_LIMIT = 50;
@@ -40,11 +41,7 @@
}
@customElement('gr-confirm-revert-dialog')
-export class GrConfirmRevertDialog extends PolymerElement {
- static get template() {
- return htmlTemplate;
- }
-
+export class GrConfirmRevertDialog extends LitElement {
/**
* Fired when the confirm button is pressed.
*
@@ -59,57 +56,154 @@
/* The revert message updated by the user
The default value is set by the dialog */
- @property({type: String})
- _message = '';
+ @state()
+ message = '';
- @property({type: Number})
- _revertType = RevertType.REVERT_SINGLE_CHANGE;
+ @state()
+ private revertType = RevertType.REVERT_SINGLE_CHANGE;
- @property({type: Boolean})
- _showRevertSubmission = false;
+ @state()
+ private showRevertSubmission = false;
- @property({type: Number})
- _changesCount?: number;
+ @state()
+ private changesCount?: number;
- @property({type: Boolean})
- _showErrorMessage = false;
+ @state()
+ showErrorMessage = false;
/* store the default revert messages per revert type so that we can
check if user has edited the revert message or not
Set when populate() is called */
- @property({type: Array})
- _originalRevertMessages: string[] = [];
+ @state()
+ private originalRevertMessages: string[] = [];
// Store the actual messages that the user has edited
- @property({type: Array})
- _revertMessages: string[] = [];
+ @state()
+ private revertMessages: string[] = [];
+
+ static override styles = [
+ sharedStyles,
+ css`
+ :host {
+ display: block;
+ }
+ :host([disabled]) {
+ opacity: 0.5;
+ pointer-events: none;
+ }
+ label {
+ cursor: pointer;
+ display: block;
+ width: 100%;
+ }
+ .revertSubmissionLayout {
+ display: flex;
+ align-items: center;
+ }
+ .label {
+ margin-left: var(--spacing-m);
+ }
+ iron-autogrow-textarea {
+ font-family: var(--monospace-font-family);
+ font-size: var(--font-size-mono);
+ line-height: var(--line-height-mono);
+ width: 73ch; /* Add a char to account for the border. */
+ }
+ .error {
+ color: var(--error-text-color);
+ margin-bottom: var(--spacing-m);
+ }
+ label[for='messageInput'] {
+ margin-top: var(--spacing-m);
+ }
+ `,
+ ];
+
+ override render() {
+ return html`
+ <gr-dialog
+ .confirmLabel=${'Revert'}
+ @confirm=${(e: Event) => this.handleConfirmTap(e)}
+ @cancel=${(e: Event) => this.handleCancelTap(e)}
+ >
+ <div class="header" slot="header">Revert Merged Change</div>
+ <div class="main" slot="main">
+ <div class="error" ?hidden=${!this.showErrorMessage}>
+ <span> A reason is required </span>
+ </div>
+ ${this.showRevertSubmission
+ ? html`
+ <div class="revertSubmissionLayout">
+ <input
+ name="revertOptions"
+ type="radio"
+ id="revertSingleChange"
+ @change=${() => this.handleRevertSingleChangeClicked()}
+ ?checked=${this.computeIfSingleRevert()}
+ />
+ <label
+ for="revertSingleChange"
+ class="label revertSingleChange"
+ >
+ Revert single change
+ </label>
+ </div>
+ <div class="revertSubmissionLayout">
+ <input
+ name="revertOptions"
+ type="radio"
+ id="revertSubmission"
+ @change=${() => this.handleRevertSubmissionClicked()}
+ .checked=${this.computeIfRevertSubmission()}
+ />
+ <label for="revertSubmission" class="label revertSubmission">
+ Revert entire submission (${this.changesCount} Changes)
+ </label>
+ </div>
+ `
+ : nothing}
+ <gr-endpoint-decorator name="confirm-revert-change">
+ <label for="messageInput"> Revert Commit Message </label>
+ <iron-autogrow-textarea
+ id="messageInput"
+ class="message"
+ .autocomplete=${'on'}
+ .maxRows=${15}
+ .bindValue=${this.message}
+ @bind-value-changed=${this.handleBindValueChanged}
+ ></iron-autogrow-textarea>
+ </gr-endpoint-decorator>
+ </div>
+ </gr-dialog>
+ `;
+ }
private readonly jsAPI = getAppContext().jsApiService;
- _computeIfSingleRevert(revertType: number) {
- return revertType === RevertType.REVERT_SINGLE_CHANGE;
+ private computeIfSingleRevert() {
+ return this.revertType === RevertType.REVERT_SINGLE_CHANGE;
}
- _computeIfRevertSubmission(revertType: number) {
- return revertType === RevertType.REVERT_SUBMISSION;
+ private computeIfRevertSubmission() {
+ return this.revertType === RevertType.REVERT_SUBMISSION;
}
- _modifyRevertMsg(change: ChangeInfo, commitMessage: string, message: string) {
+ modifyRevertMsg(change: ChangeInfo, commitMessage: string, message: string) {
return this.jsAPI.modifyRevertMsg(change, message, commitMessage);
}
populate(change: ChangeInfo, commitMessage: string, changes: ChangeInfo[]) {
- this._changesCount = changes.length;
+ this.changesCount = changes.length;
// The option to revert a single change is always available
- this._populateRevertSingleChangeMessage(
+ this.populateRevertSingleChangeMessage(
change,
commitMessage,
change.current_revision
);
- this._populateRevertSubmissionMessage(change, changes, commitMessage);
+ this.populateRevertSubmissionMessage(change, changes, commitMessage);
}
- _populateRevertSingleChangeMessage(
+ populateRevertSingleChangeMessage(
change: ChangeInfo,
commitMessage: string,
commitHash?: CommitId
@@ -127,20 +221,20 @@
`${revertTitle}\n\n${revertCommitText}\n\n` +
`Reason for revert: ${INSERT_REASON_STRING}\n`;
// This is to give plugins a chance to update message
- this._message = this._modifyRevertMsg(change, commitMessage, message);
- this._revertType = RevertType.REVERT_SINGLE_CHANGE;
- this._showRevertSubmission = false;
- this._revertMessages[this._revertType] = this._message;
- this._originalRevertMessages[this._revertType] = this._message;
+ this.message = this.modifyRevertMsg(change, commitMessage, message);
+ this.revertType = RevertType.REVERT_SINGLE_CHANGE;
+ this.showRevertSubmission = false;
+ this.revertMessages[this.revertType] = this.message;
+ this.originalRevertMessages[this.revertType] = this.message;
}
- _getTrimmedChangeSubject(subject: string) {
+ private getTrimmedChangeSubject(subject: string) {
if (!subject) return '';
if (subject.length < CHANGE_SUBJECT_LIMIT) return subject;
return subject.substring(0, CHANGE_SUBJECT_LIMIT) + '...';
}
- _modifyRevertSubmissionMsg(
+ private modifyRevertSubmissionMsg(
change: ChangeInfo,
msg: string,
commitMessage: string
@@ -148,7 +242,7 @@
return this.jsAPI.modifyRevertSubmissionMsg(change, msg, commitMessage);
}
- _populateRevertSubmissionMessage(
+ populateRevertSubmissionMessage(
change: ChangeInfo,
changes: ChangeInfo[],
commitMessage: string
@@ -170,48 +264,52 @@
changes.forEach(change => {
message +=
`${change.change_id.substring(0, 10)}:` +
- `${this._getTrimmedChangeSubject(change.subject)}\n`;
+ `${this.getTrimmedChangeSubject(change.subject)}\n`;
});
- this._message = this._modifyRevertSubmissionMsg(
+ this.message = this.modifyRevertSubmissionMsg(
change,
message,
commitMessage
);
- this._revertType = RevertType.REVERT_SUBMISSION;
- this._revertMessages[this._revertType] = this._message;
- this._originalRevertMessages[this._revertType] = this._message;
- this._showRevertSubmission = true;
+ this.revertType = RevertType.REVERT_SUBMISSION;
+ this.revertMessages[this.revertType] = this.message;
+ this.originalRevertMessages[this.revertType] = this.message;
+ this.showRevertSubmission = true;
}
- _handleRevertSingleChangeClicked() {
- this._showErrorMessage = false;
- if (this._message)
- this._revertMessages[RevertType.REVERT_SUBMISSION] = this._message;
- this._message = this._revertMessages[RevertType.REVERT_SINGLE_CHANGE];
- this._revertType = RevertType.REVERT_SINGLE_CHANGE;
+ private handleBindValueChanged(e: BindValueChangeEvent) {
+ this.message = e.detail.value;
}
- _handleRevertSubmissionClicked() {
- this._showErrorMessage = false;
- this._revertType = RevertType.REVERT_SUBMISSION;
- if (this._message)
- this._revertMessages[RevertType.REVERT_SINGLE_CHANGE] = this._message;
- this._message = this._revertMessages[RevertType.REVERT_SUBMISSION];
+ private handleRevertSingleChangeClicked() {
+ this.showErrorMessage = false;
+ if (this.message)
+ this.revertMessages[RevertType.REVERT_SUBMISSION] = this.message;
+ this.message = this.revertMessages[RevertType.REVERT_SINGLE_CHANGE];
+ this.revertType = RevertType.REVERT_SINGLE_CHANGE;
}
- _handleConfirmTap(e: Event) {
+ private handleRevertSubmissionClicked() {
+ this.showErrorMessage = false;
+ this.revertType = RevertType.REVERT_SUBMISSION;
+ if (this.message)
+ this.revertMessages[RevertType.REVERT_SINGLE_CHANGE] = this.message;
+ this.message = this.revertMessages[RevertType.REVERT_SUBMISSION];
+ }
+
+ private handleConfirmTap(e: Event) {
e.preventDefault();
e.stopPropagation();
if (
- this._message === this._originalRevertMessages[this._revertType] ||
- this._message.includes(INSERT_REASON_STRING)
+ this.message === this.originalRevertMessages[this.revertType] ||
+ this.message.includes(INSERT_REASON_STRING)
) {
- this._showErrorMessage = true;
+ this.showErrorMessage = true;
return;
}
const detail: ConfirmRevertEventDetail = {
- revertType: this._revertType,
- message: this._message,
+ revertType: this.revertType,
+ message: this.message,
};
this.dispatchEvent(
new CustomEvent('confirm', {
@@ -222,12 +320,12 @@
);
}
- _handleCancelTap(e: Event) {
+ private handleCancelTap(e: Event) {
e.preventDefault();
e.stopPropagation();
this.dispatchEvent(
new CustomEvent('cancel', {
- detail: {revertType: this._revertType},
+ detail: {revertType: this.revertType},
composed: true,
bubbles: false,
})
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-revert-dialog/gr-confirm-revert-dialog_html.ts b/polygerrit-ui/app/elements/change/gr-confirm-revert-dialog/gr-confirm-revert-dialog_html.ts
deleted file mode 100644
index b2acff2..0000000
--- a/polygerrit-ui/app/elements/change/gr-confirm-revert-dialog/gr-confirm-revert-dialog_html.ts
+++ /dev/null
@@ -1,102 +0,0 @@
-/**
- * @license
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-import {html} from '@polymer/polymer/lib/utils/html-tag';
-
-export const htmlTemplate = html`
- <style include="shared-styles">
- :host {
- display: block;
- }
- :host([disabled]) {
- opacity: 0.5;
- pointer-events: none;
- }
- label {
- cursor: pointer;
- display: block;
- width: 100%;
- }
- .revertSubmissionLayout {
- display: flex;
- align-items: center;
- }
- .label {
- margin-left: var(--spacing-m);
- }
- iron-autogrow-textarea {
- font-family: var(--monospace-font-family);
- font-size: var(--font-size-mono);
- line-height: var(--line-height-mono);
- width: 73ch; /* Add a char to account for the border. */
- }
- .error {
- color: var(--error-text-color);
- margin-bottom: var(--spacing-m);
- }
- label[for='messageInput'] {
- margin-top: var(--spacing-m);
- }
- </style>
- <gr-dialog
- confirm-label="Revert"
- on-confirm="_handleConfirmTap"
- on-cancel="_handleCancelTap"
- >
- <div class="header" slot="header">Revert Merged Change</div>
- <div class="main" slot="main">
- <div class="error" hidden$="[[!_showErrorMessage]]">
- <span> A reason is required </span>
- </div>
- <template is="dom-if" if="[[_showRevertSubmission]]">
- <div class="revertSubmissionLayout">
- <input
- name="revertOptions"
- type="radio"
- id="revertSingleChange"
- on-change="_handleRevertSingleChangeClicked"
- checked="[[_computeIfSingleRevert(_revertType)]]"
- />
- <label for="revertSingleChange" class="label revertSingleChange">
- Revert single change
- </label>
- </div>
- <div class="revertSubmissionLayout">
- <input
- name="revertOptions"
- type="radio"
- id="revertSubmission"
- on-change="_handleRevertSubmissionClicked"
- checked="[[_computeIfRevertSubmission(_revertType)]]"
- />
- <label for="revertSubmission" class="label revertSubmission">
- Revert entire submission ([[_changesCount]] Changes)
- </label>
- </div>
- </template>
- <gr-endpoint-decorator name="confirm-revert-change">
- <label for="messageInput"> Revert Commit Message </label>
- <iron-autogrow-textarea
- id="messageInput"
- class="message"
- autocomplete="on"
- max-rows="15"
- bind-value="{{_message}}"
- ></iron-autogrow-textarea>
- </gr-endpoint-decorator>
- </div>
- </gr-dialog>
-`;
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-revert-dialog/gr-confirm-revert-dialog_test.ts b/polygerrit-ui/app/elements/change/gr-confirm-revert-dialog/gr-confirm-revert-dialog_test.ts
index 38429b1..0fdcb97 100644
--- a/polygerrit-ui/app/elements/change/gr-confirm-revert-dialog/gr-confirm-revert-dialog_test.ts
+++ b/polygerrit-ui/app/elements/change/gr-confirm-revert-dialog/gr-confirm-revert-dialog_test.ts
@@ -15,26 +15,48 @@
* limitations under the License.
*/
+import {fixture, html} from '@open-wc/testing-helpers';
import '../../../test/common-test-setup-karma';
import {createChange} from '../../../test/test-data-generators';
import {CommitId} from '../../../types/common';
import './gr-confirm-revert-dialog';
import {GrConfirmRevertDialog} from './gr-confirm-revert-dialog';
-const basicFixture = fixtureFromElement('gr-confirm-revert-dialog');
-
suite('gr-confirm-revert-dialog tests', () => {
let element: GrConfirmRevertDialog;
- setup(() => {
- element = basicFixture.instantiate();
+ setup(async () => {
+ element = await fixture(
+ html`<gr-confirm-revert-dialog></gr-confirm-revert-dialog>`
+ );
+ });
+
+ test('renders', () => {
+ expect(element).shadowDom.to.equal(/* HTML */ `
+ <gr-dialog role="dialog">
+ <div class="header" slot="header">Revert Merged Change</div>
+ <div class="main" slot="main">
+ <div class="error" hidden="">
+ <span> A reason is required </span>
+ </div>
+ <gr-endpoint-decorator name="confirm-revert-change">
+ <label for="messageInput"> Revert Commit Message </label>
+ <iron-autogrow-textarea
+ id="messageInput"
+ class="message"
+ aria-disabled="false"
+ ></iron-autogrow-textarea>
+ </gr-endpoint-decorator>
+ </div>
+ </gr-dialog>
+ `);
});
test('no match', () => {
- assert.isNotOk(element._message);
+ assert.isNotOk(element.message);
const alertStub = sinon.stub();
element.addEventListener('show-alert', alertStub);
- element._populateRevertSingleChangeMessage(
+ element.populateRevertSingleChangeMessage(
createChange(),
'not a commitHash in sight',
undefined
@@ -43,8 +65,8 @@
});
test('single line', () => {
- assert.isNotOk(element._message);
- element._populateRevertSingleChangeMessage(
+ assert.isNotOk(element.message);
+ element.populateRevertSingleChangeMessage(
createChange(),
'one line commit\n\nChange-Id: abcdefg\n',
'abcd123' as CommitId
@@ -53,12 +75,12 @@
'Revert "one line commit"\n\n' +
'This reverts commit abcd123.\n\n' +
'Reason for revert: <INSERT REASONING HERE>\n';
- assert.equal(element._message, expected);
+ assert.equal(element.message, expected);
});
test('multi line', () => {
- assert.isNotOk(element._message);
- element._populateRevertSingleChangeMessage(
+ assert.isNotOk(element.message);
+ element.populateRevertSingleChangeMessage(
createChange(),
'many lines\ncommit\n\nmessage\n\nChange-Id: abcdefg\n',
'abcd123' as CommitId
@@ -67,12 +89,12 @@
'Revert "many lines"\n\n' +
'This reverts commit abcd123.\n\n' +
'Reason for revert: <INSERT REASONING HERE>\n';
- assert.equal(element._message, expected);
+ assert.equal(element.message, expected);
});
test('issue above change id', () => {
- assert.isNotOk(element._message);
- element._populateRevertSingleChangeMessage(
+ assert.isNotOk(element.message);
+ element.populateRevertSingleChangeMessage(
createChange(),
'much lines\nvery\n\ncommit\n\nBug: Issue 42\nChange-Id: abcdefg\n',
'abcd123' as CommitId
@@ -81,12 +103,12 @@
'Revert "much lines"\n\n' +
'This reverts commit abcd123.\n\n' +
'Reason for revert: <INSERT REASONING HERE>\n';
- assert.equal(element._message, expected);
+ assert.equal(element.message, expected);
});
test('revert a revert', () => {
- assert.isNotOk(element._message);
- element._populateRevertSingleChangeMessage(
+ assert.isNotOk(element.message);
+ element.populateRevertSingleChangeMessage(
createChange(),
'Revert "one line commit"\n\nChange-Id: abcdefg\n',
'abcd123' as CommitId
@@ -95,6 +117,6 @@
'Revert "Revert "one line commit""\n\n' +
'This reverts commit abcd123.\n\n' +
'Reason for revert: <INSERT REASONING HERE>\n';
- assert.equal(element._message, expected);
+ assert.equal(element.message, expected);
});
});
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-submit-dialog/gr-confirm-submit-dialog.ts b/polygerrit-ui/app/elements/change/gr-confirm-submit-dialog/gr-confirm-submit-dialog.ts
index 8cb50fd..de9395f 100644
--- a/polygerrit-ui/app/elements/change/gr-confirm-submit-dialog/gr-confirm-submit-dialog.ts
+++ b/polygerrit-ui/app/elements/change/gr-confirm-submit-dialog/gr-confirm-submit-dialog.ts
@@ -129,7 +129,7 @@
</p>
<gr-thread-list
id="commentList"
- .threads="${this.unresolvedThreads}"
+ .threads=${this.unresolvedThreads}
hide-dropdown
>
</gr-thread-list>
@@ -159,11 +159,11 @@
${this.renderChangeEdit()}
<gr-endpoint-param
name="change"
- .value="${this.change}"
+ .value=${this.change}
></gr-endpoint-param>
<gr-endpoint-param
name="action"
- .value="${this.action}"
+ .value=${this.action}
></gr-endpoint-param>
</gr-endpoint-decorator>
</div>
diff --git a/polygerrit-ui/app/elements/change/gr-download-dialog/gr-download-dialog.ts b/polygerrit-ui/app/elements/change/gr-download-dialog/gr-download-dialog.ts
index 604dc51..c1fa7d5 100644
--- a/polygerrit-ui/app/elements/change/gr-download-dialog/gr-download-dialog.ts
+++ b/polygerrit-ui/app/elements/change/gr-download-dialog/gr-download-dialog.ts
@@ -178,10 +178,10 @@
<div class="patchFiles">
<label>Patch file</label>
<div>
- <a id="download" .href="${this.computeDownloadLink()}" download>
+ <a id="download" .href=${this.computeDownloadLink()} download>
${this.computeDownloadFilename()}
</a>
- <a .href="${this.computeDownloadLink(true)}" download>
+ <a .href=${this.computeDownloadLink(true)} download>
${this.computeDownloadFilename(true)}
</a>
</div>
diff --git a/polygerrit-ui/app/elements/change/gr-label-score-row/gr-label-score-row.ts b/polygerrit-ui/app/elements/change/gr-label-score-row/gr-label-score-row.ts
index 29410f3..e2852f6 100644
--- a/polygerrit-ui/app/elements/change/gr-label-score-row/gr-label-score-row.ts
+++ b/polygerrit-ui/app/elements/change/gr-label-score-row/gr-label-score-row.ts
@@ -176,10 +176,10 @@
override render() {
return html`
<span
- class="${classMap({
+ class=${classMap({
labelNameCell: true,
newSubmitRequirements: this.isSubmitRequirementsUiEnabled,
- })}"
+ })}
id="labelName"
aria-hidden="true"
>${this.label?.name ?? ''}</span
@@ -204,7 +204,7 @@
.fill('')
.map(
() => html`
- <span class="placeholder" data-label="${this.label?.name ?? ''}">
+ <span class="placeholder" data-label=${this.label?.name ?? ''}>
</span>
`
);
@@ -215,7 +215,7 @@
<iron-selector
id="labelSelector"
.attrForSelected=${'data-value'}
- selected="${ifDefined(this._computeLabelValue())}"
+ selected=${ifDefined(this._computeLabelValue())}
@selected-item-changed=${this.setSelectedValueText}
role="radiogroup"
aria-labelledby="labelName"
@@ -231,22 +231,22 @@
(value, index) => html`
<gr-button
role="radio"
- title="${ifDefined(this.computeLabelValueTitle(value))}"
- data-vote="${this._computeVoteAttribute(
+ title=${ifDefined(this.computeLabelValueTitle(value))}
+ data-vote=${this._computeVoteAttribute(
Number(value),
index,
items.length
- )}"
- data-name="${ifDefined(this.label?.name)}"
- data-value="${value}"
- aria-label="${value}"
+ )}
+ data-name=${ifDefined(this.label?.name)}
+ data-value=${value}
+ aria-label=${value}
voteChip
flatten
>
<gr-tooltip-content
has-tooltip
light-tooltip
- title="${ifDefined(this.computeLabelValueTitle(value))}"
+ title=${ifDefined(this.computeLabelValueTitle(value))}
>
${value}
</gr-tooltip-content>
@@ -258,10 +258,10 @@
private renderSelectedValue() {
return html`
<div
- class="${classMap({
+ class=${classMap({
selectedValueCell: true,
newSubmitRequirements: this.isSubmitRequirementsUiEnabled,
- })}"
+ })}
>
<span id="selectedValueLabel">${this.selectedValueText}</span>
</div>
diff --git a/polygerrit-ui/app/elements/change/gr-label-scores/gr-label-scores.ts b/polygerrit-ui/app/elements/change/gr-label-scores/gr-label-scores.ts
index da91154..dd2a83e 100644
--- a/polygerrit-ui/app/elements/change/gr-label-scores/gr-label-scores.ts
+++ b/polygerrit-ui/app/elements/change/gr-label-scores/gr-label-scores.ts
@@ -21,7 +21,6 @@
import {
ChangeInfo,
AccountInfo,
- DetailedLabelInfo,
LabelNameToValueMap,
} from '../../../types/common';
import {GrLabelScoreRow} from '../gr-label-score-row/gr-label-score-row';
@@ -32,6 +31,7 @@
computeLabels,
Label,
computeOrderedLabelValues,
+ getDefaultValue,
} from '../../../utils/label-util';
import {ChangeStatus} from '../../../constants/constants';
import {fontStyles} from '../../../styles/gr-font-styles';
@@ -160,11 +160,13 @@
)
.map(
label => html`<gr-label-score-row
- .label="${label}"
- .name="${label.name}"
- .labels="${this.change?.labels}"
- .permittedLabels="${this.permittedLabels}"
- .orderedLabelValues="${computeOrderedLabelValues()}"
+ .label=${label}
+ .name=${label.name}
+ .labels=${this.change?.labels}
+ .permittedLabels=${this.permittedLabels}
+ .orderedLabelValues=${computeOrderedLabelValues(
+ this.permittedLabels
+ )}
></gr-label-score-row>`
)}
</div>`;
@@ -203,20 +205,13 @@
if (selectedVal === undefined) continue;
- const defValNum = this.getDefaultValue(label);
+ const defValNum = getDefaultValue(this.change?.labels, label);
if (includeDefaults || selectedVal !== defValNum) {
labels[label] = selectedVal;
}
}
return labels;
}
-
- private getDefaultValue(labelName?: string) {
- const labels = this.change?.labels;
- if (!labelName || !labels?.[labelName]) return undefined;
- const labelInfo = labels[labelName] as DetailedLabelInfo;
- return labelInfo.default_value;
- }
}
declare global {
diff --git a/polygerrit-ui/app/elements/change/gr-message-scores/gr-message-scores.ts b/polygerrit-ui/app/elements/change/gr-message-scores/gr-message-scores.ts
index 50492d2..4bf9d10 100644
--- a/polygerrit-ui/app/elements/change/gr-message-scores/gr-message-scores.ts
+++ b/polygerrit-ui/app/elements/change/gr-message-scores/gr-message-scores.ts
@@ -119,11 +119,11 @@
) {
const labels = this.change?.labels ?? {};
return html`<gr-trigger-vote
- .label="${score.label}"
+ .label=${score.label}
.displayValue=${score.value}
- .labelInfo="${labels[score.label]}"
- .change="${this.change}"
- .mutable="${false}"
+ .labelInfo=${labels[score.label]}
+ .change=${this.change}
+ .mutable=${false}
disable-hovercards
>
</gr-trigger-vote>`;
diff --git a/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-change.ts b/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-change.ts
index 744db3b..cbb29de 100644
--- a/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-change.ts
+++ b/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-change.ts
@@ -114,9 +114,9 @@
return html`
<div class="changeContainer">
<a
- href="${ifDefined(this.href)}"
- aria-label="${ifDefined(this.label)}"
- class="${linkClass}"
+ href=${ifDefined(this.href)}
+ aria-label=${ifDefined(this.label)}
+ class=${linkClass}
><slot></slot
></a>
${this.showSubmittableCheck
@@ -130,7 +130,7 @@
>`
: ''}
${this.showChangeStatus && !isChangeInfo(change)
- ? html`<span class="${this._computeChangeStatusClass(change)}">
+ ? html`<span class=${this._computeChangeStatusClass(change)}>
(${this._computeChangeStatus(change)})
</span>`
: ''}
diff --git a/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-changes-list.ts b/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-changes-list.ts
index c882764..05fe62c 100644
--- a/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-changes-list.ts
+++ b/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-changes-list.ts
@@ -202,31 +202,31 @@
return html`<section id="relatedChanges">
<gr-related-collapse
title="Relation chain"
- class="${classMap({first: isFirst})}"
+ class=${classMap({first: isFirst})}
.length=${this.relatedChanges.length}
.numChangesWhenCollapsed=${sectionSize(Section.RELATED_CHANGES)}
>
${this.relatedChanges.map(
(change, index) =>
html`<div
- class="${classMap({
+ class=${classMap({
['relatedChangeLine']: true,
['show-when-collapsed']:
relatedChangesMarkersPredicate(index).showWhenCollapsed,
- })}"
+ })}
>
${this.renderMarkers(
relatedChangesMarkersPredicate(index)
)}<gr-related-change
- .change="${change}"
- .connectedRevisions="${connectedRevisions}"
- .href="${change?._change_number
+ .change=${change}
+ .connectedRevisions=${connectedRevisions}
+ .href=${change?._change_number
? GerritNav.getUrlForChangeById(
change._change_number,
change.project,
change._revision_number as PatchSetNum
)
- : ''}"
+ : ''}
.showChangeStatus=${true}
>${change.commit.subject}</gr-related-change
>
@@ -259,28 +259,28 @@
return html`<section id="submittedTogether">
<gr-related-collapse
title="Submitted together"
- class="${classMap({first: isFirst})}"
+ class=${classMap({first: isFirst})}
.length=${submittedTogetherChanges.length}
.numChangesWhenCollapsed=${sectionSize(Section.SUBMITTED_TOGETHER)}
>
${submittedTogetherChanges.map(
(change, index) =>
html`<div
- class="${classMap({
+ class=${classMap({
['relatedChangeLine']: true,
['show-when-collapsed']:
submittedTogetherMarkersPredicate(index).showWhenCollapsed,
- })}"
+ })}
>
${this.renderMarkers(
submittedTogetherMarkersPredicate(index)
)}<gr-related-change
- .label="${this.renderChangeTitle(change)}"
- .change="${change}"
- .href="${GerritNav.getUrlForChangeById(
+ .label=${this.renderChangeTitle(change)}
+ .change=${change}
+ .href=${GerritNav.getUrlForChangeById(
change._number,
change.project
- )}"
+ )}
.showSubmittableCheck=${true}
>${this.renderChangeLine(change)}</gr-related-change
>
@@ -309,28 +309,28 @@
return html`<section id="sameTopic">
<gr-related-collapse
title="Same topic"
- class="${classMap({first: isFirst})}"
+ class=${classMap({first: isFirst})}
.length=${this.sameTopicChanges.length}
.numChangesWhenCollapsed=${sectionSize(Section.SAME_TOPIC)}
>
${this.sameTopicChanges.map(
(change, index) =>
html`<div
- class="${classMap({
+ class=${classMap({
['relatedChangeLine']: true,
['show-when-collapsed']:
sameTopicMarkersPredicate(index).showWhenCollapsed,
- })}"
+ })}
>
${this.renderMarkers(
sameTopicMarkersPredicate(index)
)}<gr-related-change
- .change="${change}"
- .label="${this.renderChangeTitle(change)}"
- .href="${GerritNav.getUrlForChangeById(
+ .change=${change}
+ .label=${this.renderChangeTitle(change)}
+ .href=${GerritNav.getUrlForChangeById(
change._number,
change.project
- )}"
+ )}
>${this.renderChangeLine(change)}</gr-related-change
>
</div>`
@@ -354,27 +354,27 @@
return html`<section id="mergeConflicts">
<gr-related-collapse
title="Merge conflicts"
- class="${classMap({first: isFirst})}"
+ class=${classMap({first: isFirst})}
.length=${this.conflictingChanges.length}
.numChangesWhenCollapsed=${sectionSize(Section.MERGE_CONFLICTS)}
>
${this.conflictingChanges.map(
(change, index) =>
html`<div
- class="${classMap({
+ class=${classMap({
['relatedChangeLine']: true,
['show-when-collapsed']:
mergeConflictsMarkersPredicate(index).showWhenCollapsed,
- })}"
+ })}
>
${this.renderMarkers(
mergeConflictsMarkersPredicate(index)
)}<gr-related-change
- .change="${change}"
- .href="${GerritNav.getUrlForChangeById(
+ .change=${change}
+ .href=${GerritNav.getUrlForChangeById(
change._number,
change.project
- )}"
+ )}
>${change.subject}</gr-related-change
>
</div>`
@@ -398,27 +398,27 @@
return html`<section id="cherryPicks">
<gr-related-collapse
title="Cherry picks"
- class="${classMap({first: isFirst})}"
+ class=${classMap({first: isFirst})}
.length=${this.cherryPickChanges.length}
.numChangesWhenCollapsed=${sectionSize(Section.CHERRY_PICKS)}
>
${this.cherryPickChanges.map(
(change, index) =>
html`<div
- class="${classMap({
+ class=${classMap({
['relatedChangeLine']: true,
['show-when-collapsed']:
cherryPicksMarkersPredicate(index).showWhenCollapsed,
- })}"
+ })}
>
${this.renderMarkers(
cherryPicksMarkersPredicate(index)
)}<gr-related-change
- .change="${change}"
- .href="${GerritNav.getUrlForChangeById(
+ .change=${change}
+ .href=${GerritNav.getUrlForChangeById(
change._number,
change.project
- )}"
+ )}
>${change.branch}: ${change.subject}</gr-related-change
>
</div>`
@@ -433,7 +433,7 @@
private renderChangeLine(change: ChangeInfo) {
const truncatedRepo = truncatePath(change.project, 2);
- return html`<span class="truncatedRepo" .title="${change.project}"
+ return html`<span class="truncatedRepo" .title=${change.project}
>${truncatedRepo}</span
>: ${change.branch}: ${change.subject}`;
}
@@ -775,7 +775,7 @@
buttonText = `Show all (${this.length})`;
buttonIcon = 'expand-more';
}
- button = html`<gr-button link="" @click="${this.toggle}"
+ button = html`<gr-button link="" @click=${this.toggle}
>${buttonText}<iron-icon icon="gr-icons:${buttonIcon}"></iron-icon
></gr-button>`;
}
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 87b699e..de03d81 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
@@ -52,9 +52,9 @@
import {
AccountAddition,
AccountInfoInput,
+ AccountInputDetail,
GrAccountList,
GroupInfoInput,
- GroupObjectInput,
RawAccountInput,
} from '../../shared/gr-account-list/gr-account-list';
import {
@@ -63,8 +63,6 @@
AttentionSetInput,
ChangeInfo,
CommentInput,
- EmailAddress,
- GroupId,
GroupInfo,
isAccount,
isDetailedLabelInfo,
@@ -78,6 +76,7 @@
ReviewInput,
ReviewResult,
ServerInfo,
+ SuggestedReviewerGroupInfo,
Suggestion,
} from '../../../types/common';
import {GrButton} from '../../shared/gr-button/gr-button';
@@ -275,7 +274,7 @@
_attentionCcsCount = 0;
@property({type: Object, observer: '_reviewerPendingConfirmationUpdated'})
- _ccPendingConfirmation: GroupObjectInput | null = null;
+ _ccPendingConfirmation: SuggestedReviewerGroupInfo | null = null;
@property({
type: String,
@@ -290,7 +289,7 @@
_uploader?: AccountInfo;
@property({type: Object})
- _pendingConfirmationDetails: GroupObjectInput | null = null;
+ _pendingConfirmationDetails: SuggestedReviewerGroupInfo | null = null;
@property({type: Boolean})
_includeComments = true;
@@ -299,7 +298,7 @@
_reviewers: (AccountInfo | GroupInfo)[] = [];
@property({type: Object, observer: '_reviewerPendingConfirmationUpdated'})
- _reviewerPendingConfirmation: GroupObjectInput | null = null;
+ _reviewerPendingConfirmation: SuggestedReviewerGroupInfo | null = null;
@property({type: Boolean, observer: '_handleHeightChanged'})
_previewFormatting = false;
@@ -409,6 +408,7 @@
// elements/shared/gr-account-list/gr-account-list.js#addAccountItem
this.$.reviewers.addAccountItem({
account: (e as CustomEvent).detail.reviewer,
+ count: 1,
});
});
@@ -502,46 +502,37 @@
return selectorEl?.selectedValue;
}
- @observe('_ccs.splices')
- _ccsChanged(splices: PolymerSpliceChange<AccountInfo[] | GroupInfo[]>) {
- this._reviewerTypeChanged(splices, ReviewerType.CC);
- }
-
- @observe('_reviewers.splices')
- _reviewersChanged(splices: PolymerSpliceChange<AccountInfo[] | GroupInfo[]>) {
- this._reviewerTypeChanged(splices, ReviewerType.REVIEWER);
- }
-
- _reviewerTypeChanged(
- splices: PolymerSpliceChange<AccountInfo[] | GroupInfo[]>,
- reviewerType: ReviewerType
+ @observe('_reviewers.splices', '_ccs.splices')
+ reviewerOrCCChanged(
+ reviewerSplices?: PolymerSpliceChange<AccountInfo[] | GroupInfo[]>,
+ ccsSplices?: PolymerSpliceChange<AccountInfo[] | GroupInfo[]>
) {
- if (splices && splices.indexSplices) {
+ if (reviewerSplices?.indexSplices || ccsSplices?.indexSplices) {
this._reviewersMutated = true;
- let key: AccountId | EmailAddress | GroupId | undefined;
- let index;
- let account;
+ }
+ }
+
+ accountAdded(e: CustomEvent<AccountInputDetail>) {
+ const account = e.detail.account;
+ const key = accountOrGroupKey(account);
+ const reviewerType =
+ (e.target as GrAccountList).getAttribute('id') === 'ccs'
+ ? ReviewerType.CC
+ : ReviewerType.REVIEWER;
+ const isReviewer = ReviewerType.REVIEWER === reviewerType;
+ const array = isReviewer ? this._ccs : this._reviewers;
+ const index = array.findIndex(
+ reviewer => accountOrGroupKey(reviewer) === key
+ );
+ if (index >= 0) {
// Remove any accounts that already exist as a CC for reviewer
// or vice versa.
- const isReviewer = ReviewerType.REVIEWER === reviewerType;
- for (const splice of splices.indexSplices) {
- for (let i = 0; i < splice.addedCount; i++) {
- account = splice.object[splice.index + i];
- key = accountOrGroupKey(account);
- const array = isReviewer ? this._ccs : this._reviewers;
- index = array.findIndex(
- account => accountOrGroupKey(account) === key
- );
- if (index >= 0) {
- this.splice(isReviewer ? '_ccs' : '_reviewers', index, 1);
- const moveFrom = isReviewer ? 'CC' : 'reviewer';
- const moveTo = isReviewer ? 'reviewer' : 'CC';
- const id = account.name || key;
- const message = `${id} moved from ${moveFrom} to ${moveTo}.`;
- fireAlert(this, message);
- }
- }
- }
+ this.splice(isReviewer ? '_ccs' : '_reviewers', index, 1);
+ const moveFrom = isReviewer ? 'CC' : 'reviewer';
+ const moveTo = isReviewer ? 'reviewer' : 'CC';
+ const id = account.name || key;
+ const message = `${id} moved from ${moveFrom} to ${moveTo}.`;
+ fireAlert(this, message);
}
}
diff --git a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_html.ts b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_html.ts
index 59dcd0e..c1d7cb1 100644
--- a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_html.ts
+++ b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_html.ts
@@ -267,6 +267,7 @@
<gr-account-list
id="reviewers"
accounts="{{_reviewers}}"
+ on-account-added="accountAdded"
removable-values="[[change.removable_reviewers]]"
filter="[[filterReviewerSuggestion]]"
pending-confirmation="{{_reviewerPendingConfirmation}}"
@@ -284,6 +285,7 @@
<gr-account-list
id="ccs"
accounts="{{_ccs}}"
+ on-account-added="accountAdded"
filter="[[filterCCSuggestion]]"
pending-confirmation="{{_ccPendingConfirmation}}"
allow-any-input=""
diff --git a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_test.ts b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_test.ts
index dc6820e..1c0768e 100644
--- a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_test.ts
+++ b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_test.ts
@@ -1138,11 +1138,13 @@
element._ccPendingConfirmation = {
group,
confirm: false,
+ count: 1,
};
} else {
element._reviewerPendingConfirmation = {
group,
confirm: false,
+ count: 1,
};
}
flush();
@@ -1203,11 +1205,13 @@
element._ccPendingConfirmation = {
group,
confirm: false,
+ count: 1,
};
} else {
element._reviewerPendingConfirmation = {
group,
confirm: false,
+ count: 1,
};
}
@@ -1524,6 +1528,11 @@
element._reviewers = [reviewer1, reviewer2, reviewer3];
element._ccs = [cc1, cc2, cc3, cc4];
element.push('_reviewers', cc1);
+ element.$.reviewers.dispatchEvent(
+ new CustomEvent('account-added', {
+ detail: {account: cc1},
+ })
+ );
flush();
assert.deepEqual(element._reviewers, [
@@ -1534,7 +1543,20 @@
]);
assert.deepEqual(element._ccs, [cc2, cc3, cc4]);
- element.push('_reviewers', cc4, cc3);
+ element.push('_reviewers', cc4);
+ element.$.reviewers.dispatchEvent(
+ new CustomEvent('account-added', {
+ detail: {account: cc4},
+ })
+ );
+ flush();
+
+ element.push('_reviewers', cc3);
+ element.$.reviewers.dispatchEvent(
+ new CustomEvent('account-added', {
+ detail: {account: cc3},
+ })
+ );
flush();
assert.deepEqual(element._reviewers, [
@@ -1613,12 +1635,31 @@
element._reviewers = [reviewer1, reviewer2, reviewer3];
element._ccs = [cc1, cc2, cc3, cc4];
element.push('_ccs', reviewer1);
+ element.$.ccs.dispatchEvent(
+ new CustomEvent('account-added', {
+ detail: {account: reviewer1},
+ })
+ );
+
flush();
assert.deepEqual(element._reviewers, [reviewer2, reviewer3]);
assert.deepEqual(element._ccs, [cc1, cc2, cc3, cc4, reviewer1]);
- element.push('_ccs', reviewer3, reviewer2);
+ element.push('_ccs', reviewer3);
+ element.$.ccs.dispatchEvent(
+ new CustomEvent('account-added', {
+ detail: {account: reviewer3},
+ })
+ );
+ flush();
+
+ element.push('_ccs', reviewer2);
+ element.$.ccs.dispatchEvent(
+ new CustomEvent('account-added', {
+ detail: {account: reviewer2},
+ })
+ );
flush();
assert.deepEqual(element._reviewers, []);
@@ -1656,6 +1697,8 @@
mutations.push(...review.reviewers!);
});
+ assert.isFalse(element._reviewersMutated);
+
// Remove and add to other field.
reviewers.dispatchEvent(
new CustomEvent('remove', {
@@ -1664,6 +1707,9 @@
bubbles: true,
})
);
+
+ assert.isTrue(element._reviewersMutated);
+
ccs.$.entry.dispatchEvent(
new CustomEvent('add', {
detail: {value: {account: reviewer1}},
diff --git a/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list.ts b/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list.ts
index 33aea05..6740977 100644
--- a/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list.ts
+++ b/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list.ts
@@ -17,138 +17,177 @@
import '../../shared/gr-account-chip/gr-account-chip';
import '../../shared/gr-button/gr-button';
import '../../shared/gr-vote-chip/gr-vote-chip';
-import '../../../styles/shared-styles';
-import {dom, EventApi} from '@polymer/polymer/lib/legacy/polymer.dom';
-import {PolymerElement} from '@polymer/polymer/polymer-element';
-import {htmlTemplate} from './gr-reviewer-list_html';
-import {customElement, property, computed, observe} from '@polymer/decorators';
+import {LitElement, html} from 'lit';
+import {customElement, property, state} from 'lit/decorators';
+
import {
ChangeInfo,
AccountInfo,
ApprovalInfo,
- Reviewers,
- AccountId,
- EmailAddress,
AccountDetailInfo,
isDetailedLabelInfo,
LabelInfo,
} from '../../../types/common';
-import {PolymerDeepPropertyChange} from '@polymer/polymer/interfaces';
-import {GrAccountChip} from '../../shared/gr-account-chip/gr-account-chip';
import {hasOwnProperty} from '../../../utils/common-util';
-import {isRemovableReviewer} from '../../../utils/change-util';
-import {ReviewerState} from '../../../constants/constants';
import {getAppContext} from '../../../services/app-context';
-import {fireAlert} from '../../../utils/event-util';
import {
getApprovalInfo,
getCodeReviewLabel,
showNewSubmitRequirements,
} from '../../../utils/label-util';
import {sortReviewers} from '../../../utils/attention-set-util';
-import {KnownExperimentId} from '../../../services/flags/flags';
-import {LabelNameToValuesMap} from '../../../api/rest-api';
+import {sharedStyles} from '../../../styles/shared-styles';
+import {css} from 'lit';
+import {nothing} from 'lit';
@customElement('gr-reviewer-list')
-export class GrReviewerList extends PolymerElement {
- static get template() {
- return htmlTemplate;
- }
-
+export class GrReviewerList extends LitElement {
/**
* Fired when the "Add reviewer..." button is tapped.
*
* @event show-reply-dialog
*/
- @property({type: Object})
- change?: ChangeInfo;
+ @property({type: Object}) change?: ChangeInfo;
- @property({type: Object})
- account?: AccountDetailInfo;
+ @property({type: Object}) account?: AccountDetailInfo;
- @property({type: Boolean, reflectToAttribute: true})
- disabled = false;
+ @property({type: Boolean, reflect: true}) disabled = false;
- @property({type: Boolean})
- mutable = false;
+ @property({type: Boolean}) mutable = false;
- @property({type: Boolean})
- reviewersOnly = false;
+ @property({type: Boolean, attribute: 'reviewers-only'}) reviewersOnly = false;
- @property({type: Boolean})
- ccsOnly = false;
+ @property({type: Boolean, attribute: 'ccs-only'}) ccsOnly = false;
- @property({type: Array})
- _displayedReviewers: AccountInfo[] = [];
+ @state() displayedReviewers: AccountInfo[] = [];
- @property({type: Array})
- _reviewers: AccountInfo[] = [];
+ @state() reviewers: AccountInfo[] = [];
- @property({type: Boolean})
- _showInput = false;
+ @state() hiddenReviewerCount?: number;
- @property({type: Object})
- _xhrPromise?: Promise<Response | undefined>;
-
- private readonly restApiService = getAppContext().restApiService;
+ @state() showAllReviewers = false;
private readonly flagsService = getAppContext().flagsService;
- @computed('ccsOnly')
- get _addLabel() {
- return this.ccsOnly ? 'Add CC' : 'Add reviewer';
+ static override get styles() {
+ return [
+ sharedStyles,
+ css`
+ :host {
+ display: block;
+ }
+ :host([disabled]) {
+ opacity: 0.8;
+ pointer-events: none;
+ }
+ .container {
+ display: block;
+ /* line-height-normal for the chips, 2px for the chip border, spacing-s
+ for the gap between lines, negative bottom margin for eliminating the
+ gap after the last line */
+ line-height: calc(var(--line-height-normal) + 2px + var(--spacing-s));
+ margin-bottom: calc(0px - var(--spacing-s));
+ }
+ .addReviewer iron-icon {
+ color: inherit;
+ --iron-icon-height: 18px;
+ --iron-icon-width: 18px;
+ }
+ .controlsContainer {
+ display: inline-block;
+ }
+ gr-button.addReviewer {
+ --gr-button-padding: 1px 0px;
+ vertical-align: top;
+ top: 1px;
+ }
+ gr-button {
+ line-height: var(--line-height-normal);
+ --gr-button-padding: 0px;
+ }
+ gr-account-chip {
+ line-height: var(--line-height-normal);
+ vertical-align: top;
+ display: inline-block;
+ }
+ gr-vote-chip {
+ --gr-vote-chip-width: 14px;
+ --gr-vote-chip-height: 14px;
+ }
+ `,
+ ];
}
- @computed('_reviewers', '_displayedReviewers')
- get _hiddenReviewerCount() {
- // Polymer 2: check for undefined
- if (
- this._reviewers === undefined ||
- this._displayedReviewers === undefined
- ) {
- return undefined;
- }
- return this._reviewers.length - this._displayedReviewers.length;
+ override render() {
+ this.displayedReviewers = this.computeDisplayedReviewers() ?? [];
+ this.hiddenReviewerCount =
+ this.reviewers.length - this.displayedReviewers.length;
+ return html`
+ <div class="container">
+ <div>
+ ${this.displayedReviewers.map(reviewer =>
+ this.renderAccountChip(reviewer)
+ )}
+ <div class="controlsContainer" ?hidden=${!this.mutable}>
+ <gr-button
+ link
+ id="addReviewer"
+ class="addReviewer"
+ @click=${this.handleAddTap}
+ title=${this.ccsOnly ? 'Add CC' : 'Add reviewer'}
+ ><iron-icon icon="gr-icons:edit"></iron-icon
+ ></gr-button>
+ </div>
+ </div>
+ <gr-button
+ class="hiddenReviewers"
+ link=""
+ ?hidden=${!this.hiddenReviewerCount}
+ @click=${() => {
+ this.showAllReviewers = true;
+ }}
+ >and ${this.hiddenReviewerCount} more</gr-button
+ >
+ </div>
+ `;
}
- /**
- * Converts change.permitted_labels to an array of hashes of label keys to
- * numeric scores.
- * Example:
- * [{
- * 'Code-Review': ['-1', ' 0', '+1']
- * }]
- * will be converted to
- * [{
- * label: 'Code-Review',
- * scores: [-1, 0, 1]
- * }]
- */
- _permittedLabelsToNumericScores(labels: LabelNameToValuesMap | undefined) {
- if (!labels) return [];
- return Object.keys(labels).map(label => {
- return {
- label,
- scores: labels[label].map(v => Number(v)),
- };
- });
+ private renderAccountChip(reviewer: AccountInfo) {
+ const change = this.change;
+ if (!change) return nothing;
+ return html`
+ <gr-account-chip
+ class="reviewer"
+ .account=${reviewer}
+ .change=${change}
+ highlightAttention
+ .voteableText=${this.computeVoteableText(reviewer)}
+ .vote=${this.computeVote(reviewer)}
+ .label=${this.computeCodeReviewLabel()}
+ >
+ ${showNewSubmitRequirements(this.flagsService, this.change)
+ ? html`<gr-vote-chip
+ slot="vote-chip"
+ .vote=${this.computeVote(reviewer)}
+ .label=${this.computeCodeReviewLabel()}
+ circle-shape
+ ></gr-vote-chip>`
+ : nothing}
+ </gr-account-chip>
+ `;
}
/**
* Returns max permitted score for reviewer.
*/
- _getReviewerPermittedScore(
- reviewer: AccountInfo,
- change: ChangeInfo,
- label: string
- ) {
+ private getReviewerPermittedScore(reviewer: AccountInfo, label: string) {
// Note (issue 7874): sometimes the "all" list is not included in change
// detail responses, even when DETAILED_LABELS is included in options.
- if (!change.labels) {
+ if (!this.change?.labels) {
return NaN;
}
- const detailedLabel = change.labels[label];
+ const detailedLabel = this.change.labels[label];
if (!isDetailedLabelInfo(detailedLabel) || !detailedLabel.all) {
return NaN;
}
@@ -166,13 +205,15 @@
return NaN;
}
- _computeVoteableText(reviewer: AccountInfo, change: ChangeInfo) {
+ // private but used in tests
+ computeVoteableText(reviewer: AccountInfo) {
+ const change = this.change;
if (!change || !change.labels) {
return '';
}
const maxScores = [];
for (const label of Object.keys(change.labels)) {
- const maxScore = this._getReviewerPermittedScore(reviewer, change, label);
+ const maxScore = this.getReviewerPermittedScore(reviewer, label);
if (isNaN(maxScore) || maxScore < 0) {
continue;
}
@@ -182,35 +223,23 @@
return maxScores.join(', ');
}
- _computeVote(
- reviewer: AccountInfo,
- change?: ChangeInfo
- ): ApprovalInfo | undefined {
- const codeReviewLabel = this._computeCodeReviewLabel(change);
+ private computeVote(reviewer: AccountInfo): ApprovalInfo | undefined {
+ const codeReviewLabel = this.computeCodeReviewLabel();
if (!codeReviewLabel || !isDetailedLabelInfo(codeReviewLabel)) return;
return getApprovalInfo(codeReviewLabel, reviewer);
}
- _computeCodeReviewLabel(change?: ChangeInfo): LabelInfo | undefined {
- if (!change || !change.labels) return;
- return getCodeReviewLabel(change.labels);
+ private computeCodeReviewLabel(): LabelInfo | undefined {
+ if (!this.change?.labels) return;
+ return getCodeReviewLabel(this.change.labels);
}
- @observe('change.reviewers.*', 'change.owner')
- _reviewersChanged(
- changeRecord: PolymerDeepPropertyChange<Reviewers, Reviewers>,
- owner: AccountInfo
- ) {
- // Polymer 2: check for undefined
- if (
- changeRecord === undefined ||
- owner === undefined ||
- this.change === undefined
- ) {
+ private computeDisplayedReviewers() {
+ if (this.change?.owner === undefined) {
return;
}
let result: AccountInfo[] = [];
- const reviewers = changeRecord.base;
+ const reviewers = this.change.reviewers;
for (const key of Object.keys(reviewers)) {
if (this.reviewersOnly && key !== 'REVIEWER') {
continue;
@@ -222,68 +251,21 @@
result = result.concat(reviewers[key]!);
}
}
- this._reviewers = result
- .filter(reviewer => reviewer._account_id !== owner._account_id)
+ this.reviewers = result
+ .filter(
+ reviewer => reviewer._account_id !== this.change?.owner._account_id
+ )
.sort((r1, r2) => sortReviewers(r1, r2, this.change, this.account));
- if (this._reviewers.length > 8) {
- this._displayedReviewers = this._reviewers.slice(0, 6);
+ if (this.reviewers.length > 8 && !this.showAllReviewers) {
+ return this.reviewers.slice(0, 6);
} else {
- this._displayedReviewers = this._reviewers;
+ return this.reviewers;
}
}
- _computeCanRemoveReviewer(reviewer: AccountInfo, mutable: boolean) {
- if (this.flagsService.isEnabled(KnownExperimentId.SUBMIT_REQUIREMENTS_UI)) {
- return false;
- }
- return mutable && isRemovableReviewer(this.change, reviewer);
- }
-
- _handleRemove(e: Event) {
- e.preventDefault();
- const target = (dom(e) as EventApi).rootTarget as GrAccountChip;
- if (!target.account || !this.change?.reviewers) return;
- const accountID = target.account._account_id || target.account.email;
- if (!accountID) return;
- const reviewers = this.change.reviewers;
- let removedAccount: AccountInfo | undefined;
- let removedType: ReviewerState | undefined;
- for (const type of [ReviewerState.REVIEWER, ReviewerState.CC]) {
- const reviewerStateByType = reviewers[type] || [];
- reviewers[type] = reviewerStateByType;
- for (let i = 0; i < reviewerStateByType.length; i++) {
- if (
- reviewerStateByType[i]._account_id === accountID ||
- reviewerStateByType[i].email === accountID
- ) {
- removedAccount = reviewerStateByType[i];
- removedType = type;
- this.splice(`change.reviewers.${type}`, i, 1);
- break;
- }
- }
- }
- const curChange = this.change;
- this.disabled = true;
- this._xhrPromise = this._removeReviewer(accountID)
- .then(response => {
- this.disabled = false;
- if (!this.change?.reviewers || this.change !== curChange) return;
- if (!response?.ok) {
- this.push(`change.reviewers.${removedType}`, removedAccount);
- fireAlert(this, `Cannot remove a ${removedType}`);
- return response;
- }
- return;
- })
- .catch((err: Error) => {
- this.disabled = false;
- throw err;
- });
- }
-
- _handleAddTap(e: Event) {
+ // private but used in tests
+ handleAddTap(e: Event) {
e.preventDefault();
const value = {
reviewersOnly: false,
@@ -303,19 +285,6 @@
})
);
}
-
- _handleViewAll() {
- this._displayedReviewers = this._reviewers;
- }
-
- _removeReviewer(id: AccountId | EmailAddress): Promise<Response | undefined> {
- if (!this.change) return Promise.resolve(undefined);
- return this.restApiService.removeChangeReviewer(this.change._number, id);
- }
-
- showNewSubmitRequirements(change?: ChangeInfo) {
- return showNewSubmitRequirements(this.flagsService, change);
- }
}
declare global {
diff --git a/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list_html.ts b/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list_html.ts
deleted file mode 100644
index b697cd5..0000000
--- a/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list_html.ts
+++ /dev/null
@@ -1,106 +0,0 @@
-/**
- * @license
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-import {html} from '@polymer/polymer/lib/utils/html-tag';
-
-export const htmlTemplate = html`
- <style include="shared-styles">
- :host {
- display: block;
- }
- :host([disabled]) {
- opacity: 0.8;
- pointer-events: none;
- }
- .container {
- display: block;
- /* line-height-normal for the chips, 2px for the chip border, spacing-s
- for the gap between lines, negative bottom margin for eliminating the
- gap after the last line */
- line-height: calc(var(--line-height-normal) + 2px + var(--spacing-s));
- margin-bottom: calc(0px - var(--spacing-s));
- }
- .addReviewer iron-icon {
- color: inherit;
- --iron-icon-height: 18px;
- --iron-icon-width: 18px;
- }
- .controlsContainer {
- display: inline-block;
- }
- gr-button.addReviewer {
- --gr-button-padding: 1px 0px;
- vertical-align: top;
- top: 1px;
- }
- gr-button {
- line-height: var(--line-height-normal);
- --gr-button-padding: 0px;
- }
- gr-account-chip {
- line-height: var(--line-height-normal);
- vertical-align: top;
- display: inline-block;
- }
- gr-vote-chip {
- --gr-vote-chip-width: 14px;
- --gr-vote-chip-height: 14px;
- }
- </style>
- <div class="container">
- <div>
- <template is="dom-repeat" items="[[_displayedReviewers]]" as="reviewer">
- <gr-account-chip
- class="reviewer"
- account="[[reviewer]]"
- change="[[change]]"
- on-remove="_handleRemove"
- highlightAttention
- voteable-text="[[_computeVoteableText(reviewer, change)]]"
- removable="[[_computeCanRemoveReviewer(reviewer, mutable)]]"
- vote="[[_computeVote(reviewer, change)]]"
- label="[[_computeCodeReviewLabel(change)]]"
- >
- <template is="dom-if" if="[[showNewSubmitRequirements(change)]]">
- <gr-vote-chip
- slot="vote-chip"
- vote="[[_computeVote(reviewer, change)]]"
- label="[[_computeCodeReviewLabel(change)]]"
- circle-shape
- ></gr-vote-chip>
- </template>
- </gr-account-chip>
- </template>
- <div class="controlsContainer" hidden$="[[!mutable]]">
- <gr-button
- link=""
- id="addReviewer"
- class="addReviewer"
- on-click="_handleAddTap"
- title="[[_addLabel]]"
- ><iron-icon icon="gr-icons:edit"></iron-icon
- ></gr-button>
- </div>
- </div>
- <gr-button
- class="hiddenReviewers"
- link=""
- hidden$="[[!_hiddenReviewerCount]]"
- on-click="_handleViewAll"
- >and [[_hiddenReviewerCount]] more</gr-button
- >
- </div>
-`;
diff --git a/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list_test.ts b/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list_test.ts
index f942443..c6b1d5f 100644
--- a/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list_test.ts
+++ b/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list_test.ts
@@ -17,42 +17,38 @@
import '../../../test/common-test-setup-karma';
import './gr-reviewer-list';
-import {
- mockPromise,
- queryAndAssert,
- stubRestApi,
-} from '../../../test/test-utils';
+import {mockPromise, queryAndAssert} from '../../../test/test-utils';
import {GrReviewerList} from './gr-reviewer-list';
import {
createAccountDetailWithId,
createChange,
createDetailedLabelInfo,
} from '../../../test/test-data-generators';
-import {tap} from '@polymer/iron-test-helpers/mock-interactions';
import {GrButton} from '../../shared/gr-button/gr-button';
import {AccountId, EmailAddress} from '../../../types/common';
-import {GrAccountChip} from '../../shared/gr-account-chip/gr-account-chip';
+import './gr-reviewer-list';
const basicFixture = fixtureFromElement('gr-reviewer-list');
suite('gr-reviewer-list tests', () => {
let element: GrReviewerList;
- setup(() => {
+ setup(async () => {
element = basicFixture.instantiate();
-
- stubRestApi('removeChangeReviewer').returns(
- Promise.resolve({...new Response(), ok: true})
- );
+ await element.updateComplete;
});
- test('controls hidden on immutable element', () => {
- flush();
+ test('controls hidden on immutable element', async () => {
element.mutable = false;
+ await element.updateComplete;
+
assert.isTrue(
queryAndAssert(element, '.controlsContainer').hasAttribute('hidden')
);
+
element.mutable = true;
+ await element.updateComplete;
+
assert.isFalse(
queryAndAssert(element, '.controlsContainer').hasAttribute('hidden')
);
@@ -63,171 +59,11 @@
element.addEventListener('show-reply-dialog', () => {
dialogShown.resolve();
});
- await flush();
- tap(queryAndAssert(element, '.addReviewer'));
+ queryAndAssert<GrButton>(element, '.addReviewer').click();
await dialogShown;
});
- test('only show remove for removable reviewers', async () => {
- element.mutable = true;
- element.change = {
- ...createChange(),
- owner: {
- ...createAccountDetailWithId(1),
- },
- reviewers: {
- REVIEWER: [
- {
- ...createAccountDetailWithId(2),
- name: 'Bojack Horseman',
- email: 'SecretariatRulez96@hotmail.com' as EmailAddress,
- },
- {
- _account_id: 3 as AccountId,
- name: 'Pinky Penguin',
- },
- ],
- CC: [
- {
- ...createAccountDetailWithId(4),
- name: 'Diane Nguyen',
- email: 'macarthurfellow2B@juno.com' as EmailAddress,
- },
- {
- email: 'test@e.mail' as EmailAddress,
- },
- ],
- },
- removable_reviewers: [
- {
- _account_id: 3 as AccountId,
- name: 'Pinky Penguin',
- },
- {
- ...createAccountDetailWithId(4),
- name: 'Diane Nguyen',
- email: 'macarthurfellow2B@juno.com' as EmailAddress,
- },
- {
- email: 'test@e.mail' as EmailAddress,
- },
- ],
- };
- await flush();
- const chips = element.root!.querySelectorAll('gr-account-chip');
- assert.equal(chips.length, 4);
-
- for (const el of Array.from(chips)) {
- const accountID = el.account!._account_id || el.account!.email;
- assert.ok(accountID);
-
- const buttonEl = queryAndAssert(el, 'gr-button');
- if (accountID === 2) {
- assert.isTrue(buttonEl.hasAttribute('hidden'));
- } else {
- assert.isFalse(buttonEl.hasAttribute('hidden'));
- }
- }
- });
-
- suite('_handleRemove', () => {
- let removeReviewerStub: sinon.SinonStub;
- let reviewersChangedSpy: sinon.SinonSpy;
-
- const reviewerWithId = {
- ...createAccountDetailWithId(2),
- name: 'Some name',
- };
-
- const reviewerWithIdAndEmail = {
- ...createAccountDetailWithId(4),
- name: 'Some other name',
- email: 'example@' as EmailAddress,
- };
-
- const reviewerWithEmailOnly = {
- email: 'example2@example' as EmailAddress,
- };
-
- let chips: GrAccountChip[];
-
- setup(() => {
- removeReviewerStub = sinon
- .stub(element, '_removeReviewer')
- .returns(Promise.resolve(new Response()));
- element.mutable = true;
-
- const allReviewers = [
- reviewerWithId,
- reviewerWithIdAndEmail,
- reviewerWithEmailOnly,
- ];
-
- element.change = {
- ...createChange(),
- owner: {
- ...createAccountDetailWithId(1),
- },
- reviewers: {
- REVIEWER: allReviewers,
- },
- removable_reviewers: allReviewers,
- };
- flush();
- chips = Array.from(element.root!.querySelectorAll('gr-account-chip'));
- assert.equal(chips.length, allReviewers.length);
- reviewersChangedSpy = sinon.spy(element, '_reviewersChanged');
- });
-
- test('_handleRemove for account with accountId only', async () => {
- const accountChip = chips.find(
- chip => chip.account!._account_id === reviewerWithId._account_id
- );
- accountChip!._handleRemoveTap(new MouseEvent('click'));
- await flush();
- assert.isTrue(removeReviewerStub.calledOnce);
- assert.isTrue(removeReviewerStub.calledWith(reviewerWithId._account_id));
- assert.isTrue(reviewersChangedSpy.called);
- expect(element.change!.reviewers.REVIEWER).to.have.deep.members([
- reviewerWithIdAndEmail,
- reviewerWithEmailOnly,
- ]);
- });
-
- test('_handleRemove for account with accountId and email', async () => {
- const accountChip = chips.find(
- chip => chip.account!._account_id === reviewerWithIdAndEmail._account_id
- );
- accountChip!._handleRemoveTap(new MouseEvent('click'));
- await flush();
- assert.isTrue(removeReviewerStub.calledOnce);
- assert.isTrue(
- removeReviewerStub.calledWith(reviewerWithIdAndEmail._account_id)
- );
- assert.isTrue(reviewersChangedSpy.called);
- expect(element.change!.reviewers.REVIEWER).to.have.deep.members([
- reviewerWithId,
- reviewerWithEmailOnly,
- ]);
- });
-
- test('_handleRemove for account with email only', async () => {
- const accountChip = chips.find(
- chip => chip.account!.email === reviewerWithEmailOnly.email
- );
- accountChip!._handleRemoveTap(new MouseEvent('click'));
- await flush();
- assert.isTrue(removeReviewerStub.calledOnce);
- assert.isTrue(removeReviewerStub.calledWith(reviewerWithEmailOnly.email));
- assert.isTrue(reviewersChangedSpy.called);
- expect(element.change!.reviewers.REVIEWER).to.have.deep.members([
- reviewerWithId,
- reviewerWithIdAndEmail,
- ]);
- });
- });
-
- test('tracking reviewers and ccs', () => {
+ test('tracking reviewers and ccs', async () => {
let counter = 0;
function makeAccount() {
return {_account_id: counter++ as AccountId};
@@ -249,7 +85,8 @@
owner,
reviewers,
};
- assert.deepEqual(element._reviewers, [reviewer, cc]);
+ await element.updateComplete;
+ assert.deepEqual(element.reviewers, [reviewer, cc]);
element.reviewersOnly = true;
element.change = {
@@ -257,7 +94,9 @@
owner,
reviewers,
};
- assert.deepEqual(element._reviewers, [reviewer]);
+ await element.updateComplete;
+
+ assert.deepEqual(element.reviewers, [reviewer]);
element.ccsOnly = true;
element.reviewersOnly = false;
@@ -266,16 +105,18 @@
owner,
reviewers,
};
- assert.deepEqual(element._reviewers, [cc]);
+ await element.updateComplete;
+
+ assert.deepEqual(element.reviewers, [cc]);
});
- test('_handleAddTap passes mode with event', () => {
+ test('handleAddTap passes mode with event', () => {
const fireStub = sinon.stub(element, 'dispatchEvent');
const e = {...new Event(''), preventDefault() {}};
element.ccsOnly = false;
element.reviewersOnly = false;
- element._handleAddTap(e);
+ element.handleAddTap(e);
assert.equal(fireStub.lastCall.args[0].type, 'show-reply-dialog');
assert.deepEqual((fireStub.lastCall.args[0] as CustomEvent).detail, {
value: {
@@ -285,7 +126,7 @@
});
element.reviewersOnly = true;
- element._handleAddTap(e);
+ element.handleAddTap(e);
assert.equal(fireStub.lastCall.args[0].type, 'show-reply-dialog');
assert.deepEqual((fireStub.lastCall.args[0] as CustomEvent).detail, {
value: {reviewersOnly: true, ccsOnly: false},
@@ -293,14 +134,14 @@
element.ccsOnly = true;
element.reviewersOnly = false;
- element._handleAddTap(e);
+ element.handleAddTap(e);
assert.equal(fireStub.lastCall.args[0].type, 'show-reply-dialog');
assert.deepEqual((fireStub.lastCall.args[0] as CustomEvent).detail, {
value: {ccsOnly: true, reviewersOnly: false},
});
});
- test('dont show all reviewers button with 4 reviewers', () => {
+ test('dont show all reviewers button with 4 reviewers', async () => {
const reviewers = [];
for (let i = 0; i < 4; i++) {
reviewers.push({
@@ -320,13 +161,15 @@
CC: reviewers,
},
};
- assert.equal(element._hiddenReviewerCount, 0);
- assert.equal(element._displayedReviewers.length, 4);
- assert.equal(element._reviewers.length, 4);
+ await element.updateComplete;
+
+ assert.equal(element.hiddenReviewerCount, 0);
+ assert.equal(element.displayedReviewers.length, 4);
+ assert.equal(element.reviewers.length, 4);
assert.isTrue(queryAndAssert<GrButton>(element, '.hiddenReviewers').hidden);
});
- test('account owner comes first in list of reviewers', () => {
+ test('account owner comes first in list of reviewers', async () => {
const reviewers = [];
for (let i = 0; i < 4; i++) {
reviewers.push({
@@ -348,11 +191,12 @@
REVIEWER: reviewers,
},
};
- flush();
- assert.equal(element._displayedReviewers[0]._account_id, 1 as AccountId);
+ await element.updateComplete;
+
+ assert.equal(element.displayedReviewers[0]._account_id, 1 as AccountId);
});
- test('show all reviewers button with 9 reviewers', () => {
+ test('show all reviewers button with 9 reviewers', async () => {
const reviewers = [];
for (let i = 0; i < 9; i++) {
reviewers.push({
@@ -372,15 +216,17 @@
CC: reviewers,
},
};
- assert.equal(element._hiddenReviewerCount, 3);
- assert.equal(element._displayedReviewers.length, 6);
- assert.equal(element._reviewers.length, 9);
+ await element.updateComplete;
+
+ assert.equal(element.hiddenReviewerCount, 3);
+ assert.equal(element.displayedReviewers.length, 6);
+ assert.equal(element.reviewers.length, 9);
assert.isFalse(
queryAndAssert<GrButton>(element, '.hiddenReviewers').hidden
);
});
- test('show all reviewers button', () => {
+ test('show all reviewers button', async () => {
const reviewers = [];
for (let i = 0; i < 100; i++) {
reviewers.push({
@@ -400,23 +246,28 @@
CC: reviewers,
},
};
- assert.equal(element._hiddenReviewerCount, 94);
- assert.equal(element._displayedReviewers.length, 6);
- assert.equal(element._reviewers.length, 100);
+
+ await element.updateComplete;
+
+ assert.equal(element.hiddenReviewerCount, 94);
+ assert.equal(element.displayedReviewers.length, 6);
+ assert.equal(element.reviewers.length, 100);
assert.isFalse(
queryAndAssert<GrButton>(element, '.hiddenReviewers').hidden
);
- tap(queryAndAssert(element, '.hiddenReviewers'));
+ queryAndAssert<GrButton>(element, '.hiddenReviewers').click();
- assert.equal(element._hiddenReviewerCount, 0);
- assert.equal(element._displayedReviewers.length, 100);
- assert.equal(element._reviewers.length, 100);
+ await element.updateComplete;
+
+ assert.equal(element.hiddenReviewerCount, 0);
+ assert.equal(element.displayedReviewers.length, 100);
+ assert.equal(element.reviewers.length, 100);
assert.isTrue(queryAndAssert<GrButton>(element, '.hiddenReviewers').hidden);
});
- test('votable labels', () => {
- const change = {
+ test('votable labels', async () => {
+ element.change = {
...createChange(),
labels: {
Foo: {
@@ -451,30 +302,33 @@
FooBar: ['-1', ' 0'],
},
};
+ await element.updateComplete;
+
assert.strictEqual(
- element._computeVoteableText({...createAccountDetailWithId(1)}, change),
+ element.computeVoteableText({...createAccountDetailWithId(1)}),
'Bar: +1'
);
assert.strictEqual(
- element._computeVoteableText({...createAccountDetailWithId(7)}, change),
+ element.computeVoteableText({...createAccountDetailWithId(7)}),
'Foo: +2, Bar: +1, FooBar: 0'
);
assert.strictEqual(
- element._computeVoteableText({...createAccountDetailWithId(2)}, change),
+ element.computeVoteableText({...createAccountDetailWithId(2)}),
''
);
});
- test('fails gracefully when all is not included', () => {
- const change = {
+ test('fails gracefully when all is not included', async () => {
+ element.change = {
...createChange(),
labels: {Foo: {}},
permitted_labels: {
Foo: ['-1', ' 0', '+1', '+2'],
},
};
+ await element.updateComplete;
assert.strictEqual(
- element._computeVoteableText({...createAccountDetailWithId(1)}, change),
+ element.computeVoteableText({...createAccountDetailWithId(1)}),
''
);
});
diff --git a/polygerrit-ui/app/elements/change/gr-submit-requirement-hovercard/gr-submit-requirement-hovercard.ts b/polygerrit-ui/app/elements/change/gr-submit-requirement-hovercard/gr-submit-requirement-hovercard.ts
index decbe93..a3ef937 100644
--- a/polygerrit-ui/app/elements/change/gr-submit-requirement-hovercard/gr-submit-requirement-hovercard.ts
+++ b/polygerrit-ui/app/elements/change/gr-submit-requirement-hovercard/gr-submit-requirement-hovercard.ts
@@ -144,7 +144,7 @@
return html` <div id="container" role="tooltip" tabindex="-1">
<div class="section">
<div class="sectionIcon">
- <iron-icon class="${icon}" icon="gr-icons:${icon}"></iron-icon>
+ <iron-icon class=${icon} icon="gr-icons:${icon}"></iron-icon>
</div>
<div class="sectionContent">
<h3 class="name heading-3">
@@ -237,7 +237,7 @@
<gr-button
link=""
id="toggleConditionsButton"
- @click="${(_: MouseEvent) => this.toggleConditionsVisibility()}"
+ @click=${(_: MouseEvent) => this.toggleConditionsVisibility()}
>
${buttonText}
<iron-icon icon="gr-icons:${icon}"></iron-icon
@@ -285,7 +285,7 @@
return html` <div class="button quickApprove">
<gr-button
link=""
- @click="${(_: MouseEvent) => this.quickApprove(labelName, maxVote)}"
+ @click=${(_: MouseEvent) => this.quickApprove(labelName, maxVote)}
>
${this.computeVoteButtonName(labelName, maxVote, type)}
</gr-button>
diff --git a/polygerrit-ui/app/elements/change/gr-submit-requirement-hovercard/gr-submit-requirement-hovercard_test.ts b/polygerrit-ui/app/elements/change/gr-submit-requirement-hovercard/gr-submit-requirement-hovercard_test.ts
index 3367eb9..ded88de 100644
--- a/polygerrit-ui/app/elements/change/gr-submit-requirement-hovercard/gr-submit-requirement-hovercard_test.ts
+++ b/polygerrit-ui/app/elements/change/gr-submit-requirement-hovercard/gr-submit-requirement-hovercard_test.ts
@@ -40,9 +40,9 @@
setup(async () => {
element = await fixture<GrSubmitRequirementHovercard>(
html`<gr-submit-requirement-hovercard
- .requirement="${createSubmitRequirementResultInfo()}"
+ .requirement=${createSubmitRequirementResultInfo()}
.change=${createChange()}
- .account="${createAccountWithId()}"
+ .account=${createAccountWithId()}
></gr-submit-requirement-hovercard>`
);
});
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 46e600e..6e60a22 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
@@ -93,6 +93,7 @@
iron-icon {
width: var(--line-height-normal, 20px);
height: var(--line-height-normal, 20px);
+ vertical-align: top;
}
.requirements,
section.trigger-votes {
@@ -122,13 +123,6 @@
.votes-cell {
display: flex;
}
- .check-error {
- margin-right: var(--spacing-l);
- }
- .check-error iron-icon {
- color: var(--error-foreground);
- vertical-align: top;
- }
gr-vote-chip {
margin-right: var(--spacing-s);
}
@@ -182,10 +176,10 @@
(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}"
+ .requirement=${requirement}
+ .change=${this.change}
+ .account=${this.account}
+ .mutable=${this.mutable ?? false}
></gr-submit-requirement-hovercard>
`
)}
@@ -199,7 +193,7 @@
<gr-limited-text
class="name"
limit="25"
- .text="${requirement.name}"
+ .text=${requirement.name}
></gr-limited-text>
</td>
<td>
@@ -237,10 +231,7 @@
return html`<div class="votes-cell">${slot}</div>`;
const endpointName = this.calculateEndpointName(requirement.name);
- return html`<gr-endpoint-decorator
- class="votes-cell"
- name="${endpointName}"
- >
+ return html`<gr-endpoint-decorator class="votes-cell" name=${endpointName}>
<gr-endpoint-param
name="change"
.value=${this.change}
@@ -256,10 +247,10 @@
renderStatus(status: SubmitRequirementStatus) {
const icon = iconForStatus(status);
return html`<iron-icon
- class="${icon}"
+ class=${icon}
icon="gr-icons:${icon}"
role="img"
- aria-label="${status.toLowerCase()}"
+ aria-label=${status.toLowerCase()}
></iron-icon>`;
}
@@ -304,15 +295,15 @@
return uniqueApprovals.map(
approvalInfo =>
html`<gr-vote-chip
- .vote="${approvalInfo}"
- .label="${labelInfo}"
- .more="${(labelInfo.all ?? []).filter(
+ .vote=${approvalInfo}
+ .label=${labelInfo}
+ .more=${(labelInfo.all ?? []).filter(
other => other.value === approvalInfo.value
- ).length > 1}"
+ ).length > 1}
></gr-vote-chip>`
);
} else if (isQuickLabelInfo(labelInfo)) {
- return [html`<gr-vote-chip .label="${labelInfo}"></gr-vote-chip>`];
+ return [html`<gr-vote-chip .label=${labelInfo}></gr-vote-chip>`];
} else {
return html``;
}
@@ -338,13 +329,13 @@
.text=${`${runsCount}`}
.links=${links}
.statusOrCategory=${Category.ERROR}
- @click="${() => {
+ @click=${() => {
fireShowPrimaryTab(this, PrimaryTab.CHECKS, false, {
checksTab: {
statusOrCategory: Category.ERROR,
},
});
- }}"
+ }}
></gr-checks-chip>`;
}
@@ -373,11 +364,11 @@
${triggerVotes.map(
label =>
html`<gr-trigger-vote
- .label="${label}"
- .labelInfo="${labels[label]}"
- .change="${this.change}"
- .account="${this.account}"
- .mutable="${this.mutable ?? false}"
+ .label=${label}
+ .labelInfo=${labels[label]}
+ .change=${this.change}
+ .account=${this.account}
+ .mutable=${this.mutable ?? false}
.disableHovercards=${this.disableHovercards}
></gr-trigger-vote>`
)}
diff --git a/polygerrit-ui/app/elements/change/gr-thread-list/gr-thread-list.ts b/polygerrit-ui/app/elements/change/gr-thread-list/gr-thread-list.ts
index 477f579..bf85d11 100644
--- a/polygerrit-ui/app/elements/change/gr-thread-list/gr-thread-list.ts
+++ b/polygerrit-ui/app/elements/change/gr-thread-list/gr-thread-list.ts
@@ -330,19 +330,19 @@
<span class="sort-text">Sort By:</span>
<gr-dropdown-list
id="sortDropdown"
- .value="${this.sortDropdownValue}"
- @value-change="${(e: CustomEvent) =>
- (this.sortDropdownValue = e.detail.value)}"
- .items="${this.getSortDropdownEntries()}"
+ .value=${this.sortDropdownValue}
+ @value-change=${(e: CustomEvent) =>
+ (this.sortDropdownValue = e.detail.value)}
+ .items=${this.getSortDropdownEntries()}
>
</gr-dropdown-list>
<span class="separator"></span>
<span class="filter-text">Filter By:</span>
<gr-dropdown-list
id="filterDropdown"
- .value="${this.getCommentsDropdownValue()}"
- @value-change="${this.handleCommentsDropdownValueChange}"
- .items="${this.getCommentsDropdownEntries()}"
+ .value=${this.getCommentsDropdownValue()}
+ @value-change=${this.handleCommentsDropdownValueChange}
+ .items=${this.getCommentsDropdownEntries()}
>
</gr-dropdown-list>
${this.renderAuthorChips()}
@@ -362,7 +362,7 @@
<gr-button
class="show-resolved-comments"
link
- @click="${this.handleAllComments}"
+ @click=${this.handleAllComments}
>Show ${pluralize(threads.length, 'resolved comment')}</gr-button
>
`;
@@ -398,16 +398,16 @@
private renderCommentThread(thread: CommentThread, isFirst: boolean) {
return html`
<gr-comment-thread
- .thread="${thread}"
+ .thread=${thread}
show-file-path
- ?show-ported-comment="${thread.ported}"
- ?show-comment-context="${this.showCommentContext}"
- ?show-file-name="${isFirst}"
- .messageId="${this.messageId}"
- ?should-scroll-into-view="${thread.rootId === this.scrollCommentId}"
- @comment-thread-editing-changed="${() => {
+ ?show-ported-comment=${thread.ported}
+ ?show-comment-context=${this.showCommentContext}
+ ?show-file-name=${isFirst}
+ .messageId=${this.messageId}
+ ?should-scroll-into-view=${thread.rootId === this.scrollCommentId}
+ @comment-thread-editing-changed=${() => {
this.requestUpdate();
- }}"
+ }}
></gr-comment-thread>
`;
}
@@ -426,11 +426,11 @@
);
return html`
<gr-account-label
- .account="${account}"
- @click="${this.handleAccountClicked}"
+ .account=${account}
+ @click=${this.handleAccountClicked}
selectionChipStyle
noStatusIcons
- ?selected="${selected}"
+ ?selected=${selected}
></gr-account-label>
`;
}
diff --git a/polygerrit-ui/app/elements/change/gr-trigger-vote/gr-trigger-vote.ts b/polygerrit-ui/app/elements/change/gr-trigger-vote/gr-trigger-vote.ts
index 41964c2..f2bd350 100644
--- a/polygerrit-ui/app/elements/change/gr-trigger-vote/gr-trigger-vote.ts
+++ b/polygerrit-ui/app/elements/change/gr-trigger-vote/gr-trigger-vote.ts
@@ -133,12 +133,12 @@
);
return approvals.map(
approvalInfo => html`<gr-vote-chip
- .vote="${approvalInfo}"
- .label="${labelInfo}"
+ .vote=${approvalInfo}
+ .label=${labelInfo}
></gr-vote-chip>`
);
} else if (isQuickLabelInfo(labelInfo)) {
- return [html`<gr-vote-chip .label="${this.labelInfo}"></gr-vote-chip>`];
+ return [html`<gr-vote-chip .label=${this.labelInfo}></gr-vote-chip>`];
} else {
return html``;
}
diff --git a/polygerrit-ui/app/elements/change/gr-trigger-vote/gr-trigger-vote_test.ts b/polygerrit-ui/app/elements/change/gr-trigger-vote/gr-trigger-vote_test.ts
index f72ebf4..8497ff4 100644
--- a/polygerrit-ui/app/elements/change/gr-trigger-vote/gr-trigger-vote_test.ts
+++ b/polygerrit-ui/app/elements/change/gr-trigger-vote/gr-trigger-vote_test.ts
@@ -63,11 +63,11 @@
const labelInfo = change?.labels?.[label];
element = await fixture<GrTriggerVote>(
html`<gr-trigger-vote
- .label="${label}"
- .labelInfo="${labelInfo}"
- .change="${change}"
- .account="${account}"
- .mutable="${false}"
+ .label=${label}
+ .labelInfo=${labelInfo}
+ .change=${change}
+ .account=${account}
+ .mutable=${false}
></gr-trigger-vote>`
);
});
diff --git a/polygerrit-ui/app/elements/checks/gr-checks-action.ts b/polygerrit-ui/app/elements/checks/gr-checks-action.ts
index 29fe368..6097cde 100644
--- a/polygerrit-ui/app/elements/checks/gr-checks-action.ts
+++ b/polygerrit-ui/app/elements/checks/gr-checks-action.ts
@@ -63,9 +63,9 @@
return html`
<gr-button
link
- ?disabled="${this.action.disabled}"
+ ?disabled=${this.action.disabled}
class="action"
- @click="${(e: Event) => this.handleClick(e)}"
+ @click=${(e: Event) => this.handleClick(e)}
>
${this.action.name}
</gr-button>
diff --git a/polygerrit-ui/app/elements/checks/gr-checks-results.ts b/polygerrit-ui/app/elements/checks/gr-checks-results.ts
index 85c0e4a..9ea29b0 100644
--- a/polygerrit-ui/app/elements/checks/gr-checks-results.ts
+++ b/polygerrit-ui/app/elements/checks/gr-checks-results.ts
@@ -333,16 +333,16 @@
`;
}
return html`
- <tr class="${classMap({container: true, collapsed: !this.isExpanded})}">
- <td class="nameCol" @click="${this.toggleExpandedClick}">
+ <tr class=${classMap({container: true, collapsed: !this.isExpanded})}>
+ <td class="nameCol" @click=${this.toggleExpandedClick}>
<div class="flex">
- <gr-hovercard-run .run="${this.result}"></gr-hovercard-run>
+ <gr-hovercard-run .run=${this.result}></gr-hovercard-run>
<div
class="name"
role="button"
tabindex="0"
- @click="${this.toggleExpandedClick}"
- @keydown="${this.toggleExpandedPress}"
+ @click=${this.toggleExpandedClick}
+ @keydown=${this.toggleExpandedPress}
>
${this.result.checkName}
</div>
@@ -353,7 +353,7 @@
<div class="summary-cell">
${this.renderLink(firstPrimaryLink(this.result))}
${this.renderSummary(this.result.summary)}
- <div class="message" @click="${this.toggleExpandedClick}">
+ <div class="message" @click=${this.toggleExpandedClick}>
${this.isExpanded ? '' : this.result.message}
</div>
${this.renderLinks()} ${this.renderActions()}
@@ -363,27 +363,27 @@
${this.renderLabel()}
</div>
</td>
- <td class="expanderCol" @click="${this.toggleExpandedClick}">
+ <td class="expanderCol" @click=${this.toggleExpandedClick}>
<div
class="show-hide"
role="switch"
tabindex="0"
- ?hidden="${!this.isExpandable}"
- aria-checked="${this.isExpanded ? 'true' : 'false'}"
- aria-label="${this.isExpanded
+ ?hidden=${!this.isExpandable}
+ aria-checked=${this.isExpanded ? 'true' : 'false'}
+ aria-label=${this.isExpanded
? 'Collapse result row'
- : 'Expand result row'}"
- @keydown="${this.toggleExpandedPress}"
+ : 'Expand result row'}
+ @keydown=${this.toggleExpandedPress}
>
<iron-icon
- icon="${this.isExpanded
+ icon=${this.isExpanded
? 'gr-icons:expand-less'
- : 'gr-icons:expand-more'}"
+ : 'gr-icons:expand-more'}
></iron-icon>
</div>
</td>
</tr>
- <tr class="${classMap({detailsRow: true, collapsed: !this.isExpanded})}">
+ <tr class=${classMap({detailsRow: true, collapsed: !this.isExpanded})}>
<td class="expandedCol" colspan="3">${this.renderExpanded()}</td>
</tr>
`;
@@ -392,7 +392,7 @@
private renderExpanded() {
if (!this.isExpanded) return;
return html`<gr-result-expanded
- .result="${this.result}"
+ .result=${this.result}
></gr-result-expanded>`;
}
@@ -437,7 +437,7 @@
return html`
<!-- The is for being able to shrink a tiny amount without
the text itself getting shrunk with an ellipsis. -->
- <div class="summary" @click="${this.toggleExpanded}">${text} </div>
+ <div class="summary" @click=${this.toggleExpanded}>${text} </div>
`;
}
@@ -457,7 +457,7 @@
return html`
<div class="label ${status}">
<span>${label} ${valueStr}</span>
- <paper-tooltip offset="5" ?fitToVisibleBounds="${true}">
+ <paper-tooltip offset="5" ?fitToVisibleBounds=${true}>
The check result has (probably) influenced this label vote.
</paper-tooltip>
</div>
@@ -484,7 +484,7 @@
if (this.isExpanded) return;
if (!link) return;
const tooltipText = link.tooltip ?? tooltipForLink(link.icon);
- return html`<a href="${link.url}" class="link" target="_blank"
+ return html`<a href=${link.url} class="link" target="_blank"
><iron-icon
aria-label="external link to details"
class="link"
@@ -510,12 +510,12 @@
link=""
vertical-offset="32"
horizontal-align="right"
- @tap-item="${this.handleAction}"
- @opened-changed="${(e: CustomEvent) =>
- toggleClass(this, 'dropdown-open', e.detail.value)}"
- ?hidden="${overflowItems.length === 0}"
- .items="${overflowItems}"
- .disabledIds="${disabledItems}"
+ @tap-item=${this.handleAction}
+ @opened-changed=${(e: CustomEvent) =>
+ toggleClass(this, 'dropdown-open', e.detail.value)}
+ ?hidden=${overflowItems.length === 0}
+ .items=${overflowItems}
+ .disabledIds=${disabledItems}
>
<iron-icon icon="gr-icons:more-vert" aria-labelledby="moreMessage">
</iron-icon>
@@ -536,7 +536,7 @@
if (!action) return;
return html`<gr-checks-action
context="result-row"
- .action="${action}"
+ .action=${action}
></gr-checks-action>`;
}
@@ -561,10 +561,10 @@
renderTag(tag: Tag) {
return html`<button
class="tag ${tag.color}"
- @click="${(e: MouseEvent) => this.tagClick(e, tag.name)}"
+ @click=${(e: MouseEvent) => this.tagClick(e, tag.name)}
>
<span>${tag.name}</span>
- <paper-tooltip offset="5" ?fitToVisibleBounds="${true}">
+ <paper-tooltip offset="5" ?fitToVisibleBounds=${true}>
${tag.tooltip ??
'A category tag for this check result. Click to filter.'}
</paper-tooltip>
@@ -624,21 +624,18 @@
${this.renderSecondaryLinks()} ${this.renderCodePointers()}
<gr-endpoint-decorator
name="check-result-expanded"
- .targetPlugin="${this.result.pluginName}"
+ .targetPlugin=${this.result.pluginName}
>
- <gr-endpoint-param
- name="run"
- .value="${this.result}"
- ></gr-endpoint-param>
+ <gr-endpoint-param name="run" .value=${this.result}></gr-endpoint-param>
<gr-endpoint-param
name="result"
- .value="${this.result}"
+ .value=${this.result}
></gr-endpoint-param>
<gr-formatted-text
noTrailingMargin
class="message"
- .content="${this.result.message}"
- .config="${this.repoConfig?.commentlinks}"
+ .content=${this.result.message}
+ .config=${this.repoConfig?.commentlinks}
></gr-formatted-text>
</gr-endpoint-decorator>
`;
@@ -697,7 +694,7 @@
if (!link) return;
const text = link.tooltip ?? tooltipForLink(link.icon);
const target = targetBlank ? '_blank' : undefined;
- return html`<a href="${link.url}" target="${ifDefined(target)}">
+ return html`<a href=${link.url} target=${ifDefined(target)}>
<iron-icon
class="link"
icon="gr-icons:${iconForLink(link.icon)}"
@@ -1056,27 +1053,27 @@
notLatest: !!this.checksPatchsetNumber,
};
return html`
- <div class="${classMap(headerClasses)}">
+ <div class=${classMap(headerClasses)}>
<div class="headerTopRow">
<div class="left">
<h2 class="heading-2">Results</h2>
- <div class="loading" ?hidden="${!this.someProvidersAreLoading}">
+ <div class="loading" ?hidden=${!this.someProvidersAreLoading}>
<span>Loading results </span>
<span class="loadingSpin"></span>
</div>
</div>
<div class="right">
<div class="goToLatest">
- <gr-button @click="${this.goToLatestPatchset}" link
+ <gr-button @click=${this.goToLatestPatchset} link
>Go to latest patchset</gr-button
>
</div>
<gr-dropdown-list
- value="${this.checksPatchsetNumber ??
+ value=${this.checksPatchsetNumber ??
this.latestPatchsetNumber ??
- 0}"
- .items="${this.createPatchsetDropdownItems()}"
- @value-change="${this.onPatchsetSelected}"
+ 0}
+ .items=${this.createPatchsetDropdownItems()}
+ @value-change=${this.onPatchsetSelected}
></gr-dropdown-list>
</div>
</div>
@@ -1141,9 +1138,9 @@
private renderLink(link?: Link) {
if (!link) return;
const tooltipText = link.tooltip ?? tooltipForLink(link.icon);
- return html`<a href="${link.url}" target="_blank"
+ return html`<a href=${link.url} target="_blank"
><iron-icon
- aria-label="${tooltipText}"
+ aria-label=${tooltipText}
class="link"
icon="gr-icons:${iconForLink(link.icon)}"
></iron-icon
@@ -1159,9 +1156,9 @@
link=""
vertical-offset="32"
horizontal-align="right"
- @tap-item="${this.handleAction}"
- .items="${items}"
- .disabledIds="${disabledIds}"
+ @tap-item=${this.handleAction}
+ .items=${items}
+ .disabledIds=${disabledIds}
>
<iron-icon icon="gr-icons:more-vert" aria-labelledby="moreMessage">
</iron-icon>
@@ -1190,7 +1187,7 @@
if (!action) return;
return html`<gr-checks-action
context="results"
- .action="${action}"
+ .action=${action}
></gr-checks-action>`;
}
@@ -1241,7 +1238,7 @@
id="filterInput"
type="text"
placeholder="Filter results by tag or regular expression"
- @input="${this.onFilterInputChange}"
+ @input=${this.onFilterInputChange}
/>
</div>
`;
@@ -1295,12 +1292,12 @@
resultCount
);
return html`
- <div class="${expandedClass}">
+ <div class=${expandedClass}>
<h3
class="categoryHeader ${catString} ${empty} heading-3"
- @click="${() => this.toggleExpanded(category)}"
+ @click=${() => this.toggleExpanded(category)}
>
- <iron-icon class="expandIcon" icon="${icon}"></iron-icon>
+ <iron-icon class="expandIcon" icon=${icon}></iron-icon>
<div class="statusIconWrapper">
<iron-icon
icon="gr-icons:${iconFor(category)}"
@@ -1336,7 +1333,7 @@
return html`
<tr class="showAllRow">
<td colspan="3">
- <gr-button class="showAll" link @click="${handler}"
+ <gr-button class="showAll" link @click=${handler}
>${message}</gr-button
>
</td>
@@ -1386,7 +1383,7 @@
<table class="resultsTable">
<thead>
<tr class="headerRow">
- <th class="${classMap(nameColClasses)}">Run</th>
+ <th class=${classMap(nameColClasses)}>Run</th>
<th class="summaryCol">Summary</th>
<th class="expanderCol"></th>
</tr>
@@ -1397,8 +1394,8 @@
result => result.internalResultId,
(result?: RunResult) => html`
<gr-result-row
- class="${charsOnly(result!.checkName)}"
- .result="${result}"
+ class=${charsOnly(result!.checkName)}
+ .result=${result}
></gr-result-row>
`
)}
diff --git a/polygerrit-ui/app/elements/checks/gr-checks-runs.ts b/polygerrit-ui/app/elements/checks/gr-checks-runs.ts
index bf77c1b..81d757e 100644
--- a/polygerrit-ui/app/elements/checks/gr-checks-runs.ts
+++ b/polygerrit-ui/app/elements/checks/gr-checks-runs.ts
@@ -245,35 +245,35 @@
return html`
<div
- @click="${this.handleChipClick}"
- @keydown="${this.handleChipKey}"
- class="${classMap(classes)}"
+ @click=${this.handleChipClick}
+ @keydown=${this.handleChipKey}
+ class=${classMap(classes)}
tabindex="0"
>
<div class="left">
- <gr-hovercard-run .run="${this.run}"></gr-hovercard-run>
+ <gr-hovercard-run .run=${this.run}></gr-hovercard-run>
${this.renderFilterIcon()}
- <iron-icon class="${icon}" icon="gr-icons:${icon}"></iron-icon>
+ <iron-icon class=${icon} icon="gr-icons:${icon}"></iron-icon>
${this.renderAdditionalIcon()}
<span class="name">${this.run.checkName}</span>
${this.renderETA()}
</div>
<div class="middle">
- <gr-checks-attempt .run="${this.run}"></gr-checks-attempt>
+ <gr-checks-attempt .run=${this.run}></gr-checks-attempt>
${this.renderStatusLink()}
</div>
<div class="right">
${action
? html`<gr-checks-action
context="runs"
- .action="${action}"
+ .action=${action}
></gr-checks-action>`
: ''}
</div>
</div>
<div
class="attemptDetails"
- ?hidden="${this.run.isSingleAttempt || !this.selected}"
+ ?hidden=${this.run.isSingleAttempt || !this.selected}
>
${this.run.attemptDetails.map(a => this.renderAttempt(a))}
</div>
@@ -295,14 +295,14 @@
return html`<div class="attemptDetail">
<input
type="radio"
- id="${id}"
- name="${`${checkNameId}-attempt-choice`}"
- ?checked="${this.isSelected(detail)}"
- ?disabled="${!this.isSelected(detail) && wasNotRun}"
- @change="${() => this.handleAttemptChange(detail)}"
+ id=${id}
+ name=${`${checkNameId}-attempt-choice`}
+ ?checked=${this.isSelected(detail)}
+ ?disabled=${!this.isSelected(detail) && wasNotRun}
+ @change=${() => this.handleAttemptChange(detail)}
/>
- <iron-icon class="${icon}" icon="gr-icons:${icon}"></iron-icon>
- <label for="${id}">
+ <iron-icon class=${icon} icon="gr-icons:${icon}"></iron-icon>
+ <label for=${id}>
Attempt ${detail.attempt}${wasNotRun ? ' (not run)' : ''}
</label>
</div>`;
@@ -325,7 +325,7 @@
const link = this.run.statusLink;
if (!link) return;
return html`
- <a href="${link}" target="_blank" @click="${this.onLinkClick}"
+ <a href=${link} target="_blank" @click=${this.onLinkClick}
><iron-icon
class="statusLinkIcon"
icon="gr-icons:launch"
@@ -362,7 +362,7 @@
if (!category) return nothing;
const icon = iconFor(category);
return html`
- <iron-icon class="${icon}" icon="gr-icons:${icon}"></iron-icon>
+ <iron-icon class=${icon} icon="gr-icons:${icon}"></iron-icon>
`;
}
@@ -597,8 +597,8 @@
id="filterInput"
type="text"
placeholder="Filter runs by regular expression"
- ?hidden="${!this.showFilter()}"
- @input="${this.onInput}"
+ ?hidden=${!this.showFilter()}
+ @input=${this.onInput}
/>
${this.renderSection(RunStatus.RUNNING)}
${this.renderSection(RunStatus.COMPLETED)}
@@ -641,7 +641,7 @@
Sign in to Checks Plugin to see runs and results
</div>
<div class="buttonRow">
- <gr-button @click="${this.loginCallback}" link>Sign in</gr-button>
+ <gr-button @click=${this.loginCallback} link>Sign in</gr-button>
</div>
</div>
`;
@@ -664,20 +664,20 @@
<gr-button
class="font-normal"
link
- @click="${() => fireRunSelectionReset(this)}"
+ @click=${() => fireRunSelectionReset(this)}
>Unselect All</gr-button
>
<gr-tooltip-content
- title="${runButtonDisabled
+ title=${runButtonDisabled
? 'Disabled. Unselect checks without a "Run" action to enable the button.'
- : ''}"
+ : ''}
?has-tooltip=${runButtonDisabled}
>
<gr-button
class="font-normal"
link
?disabled=${runButtonDisabled}
- @click="${() => {
+ @click=${() => {
actions.forEach(action => {
if (!action) return;
this.getChecksModel().triggerAction(
@@ -689,7 +689,7 @@
this.reporting.reportInteraction(
Interaction.CHECKS_RUNS_SELECTED_TRIGGERED
);
- }}"
+ }}
>Run Selected</gr-button
>
</gr-tooltip-content>
@@ -700,22 +700,22 @@
return html`
<gr-tooltip-content
has-tooltip
- title="${this.collapsed ? 'Expand runs panel' : 'Collapse runs panel'}"
+ title=${this.collapsed ? 'Expand runs panel' : 'Collapse runs panel'}
>
<gr-button
link
class="expandButton"
role="switch"
- aria-checked="${this.collapsed ? 'true' : 'false'}"
- aria-label="${this.collapsed
+ aria-checked=${this.collapsed ? 'true' : 'false'}
+ aria-label=${this.collapsed
? 'Expand runs panel'
- : 'Collapse runs panel'}"
- @click="${this.toggleCollapsed}"
+ : 'Collapse runs panel'}
+ @click=${this.toggleCollapsed}
><iron-icon
class="expandIcon"
- icon="${this.collapsed
+ icon=${this.collapsed
? 'gr-icons:chevron-right'
- : 'gr-icons:chevron-left'}"
+ : 'gr-icons:chevron-left'}
></iron-icon>
</gr-button>
</gr-tooltip-content>
@@ -781,11 +781,8 @@
}
return html`
<div class="${status.toLowerCase()} ${expandedClass}">
- <div
- class="sectionHeader"
- @click="${() => this.toggleExpanded(status)}"
- >
- <iron-icon class="expandIcon" icon="${icon}"></iron-icon>
+ <div class="sectionHeader" @click=${() => this.toggleExpanded(status)}>
+ <iron-icon class="expandIcon" icon=${icon}></iron-icon>
<h3 class="heading-3">${header}</h3>
</div>
<div class="sectionRuns">${runs.map(run => this.renderRun(run))}</div>
@@ -808,10 +805,10 @@
const selectedAttempt = this.selectedAttempts.get(run.checkName);
const deselected = !selectedRun && this.selectedRuns.length > 0;
return html`<gr-checks-run
- .run="${run}"
- .selected="${selectedRun}"
- .selectedAttempt="${selectedAttempt}"
- .deselected="${deselected}"
+ .run=${run}
+ .selected=${selectedRun}
+ .selectedAttempt=${selectedAttempt}
+ .deselected=${deselected}
></gr-checks-run>`;
}
@@ -824,39 +821,31 @@
return html`
<div class="testing">
<div>Toggle fake runs by clicking buttons:</div>
- <gr-button
- link
- @click="${() => clearAllFakeRuns(this.getChecksModel())}"
+ <gr-button link @click=${() => clearAllFakeRuns(this.getChecksModel())}
>none</gr-button
>
<gr-button
link
- @click="${() =>
- this.toggle(
- 'f0',
- [fakeRun0],
- fakeActions,
- fakeLinks,
- 'ETA: 1 min'
- )}"
+ @click=${() =>
+ this.toggle('f0', [fakeRun0], fakeActions, fakeLinks, 'ETA: 1 min')}
>0</gr-button
>
- <gr-button link @click="${() => this.toggle('f1', [fakeRun1])}"
+ <gr-button link @click=${() => this.toggle('f1', [fakeRun1])}
>1</gr-button
>
- <gr-button link @click="${() => this.toggle('f2', [fakeRun2])}"
+ <gr-button link @click=${() => this.toggle('f2', [fakeRun2])}
>2</gr-button
>
- <gr-button link @click="${() => this.toggle('f3', [fakeRun3])}"
+ <gr-button link @click=${() => this.toggle('f3', [fakeRun3])}
>3</gr-button
>
<gr-button link @click="${() => this.toggle('f4', fakeRun4Att)}}"
>4</gr-button
>
- <gr-button link @click="${() => this.toggle('f5', [fakeRun5])}"
+ <gr-button link @click=${() => this.toggle('f5', [fakeRun5])}
>5</gr-button
>
- <gr-button link @click="${() => setAllFakeRuns(this.getChecksModel())}"
+ <gr-button link @click=${() => setAllFakeRuns(this.getChecksModel())}
>all</gr-button
>
</div>
diff --git a/polygerrit-ui/app/elements/checks/gr-checks-tab.ts b/polygerrit-ui/app/elements/checks/gr-checks-tab.ts
index 2f9592f..d808d11 100644
--- a/polygerrit-ui/app/elements/checks/gr-checks-tab.ts
+++ b/polygerrit-ui/app/elements/checks/gr-checks-tab.ts
@@ -133,21 +133,21 @@
<div class="container">
<gr-checks-runs
class="runs"
- ?collapsed="${this.offsetWidth < 1000}"
- .runs="${this.runs}"
- .selectedRuns="${this.selectedRuns}"
- .selectedAttempts="${this.selectedAttempts}"
- .tabState="${this.tabState?.checksTab}"
- @run-selected="${this.handleRunSelected}"
- @attempt-selected="${this.handleAttemptSelected}"
+ ?collapsed=${this.offsetWidth < 1000}
+ .runs=${this.runs}
+ .selectedRuns=${this.selectedRuns}
+ .selectedAttempts=${this.selectedAttempts}
+ .tabState=${this.tabState?.checksTab}
+ @run-selected=${this.handleRunSelected}
+ @attempt-selected=${this.handleAttemptSelected}
></gr-checks-runs>
<gr-checks-results
class="results"
- .tabState="${this.tabState?.checksTab}"
- .runs="${this.runs}"
- .selectedRuns="${this.selectedRuns}"
- .selectedAttempts="${this.selectedAttempts}"
- @run-selected="${this.handleRunSelected}"
+ .tabState=${this.tabState?.checksTab}
+ .runs=${this.runs}
+ .selectedRuns=${this.selectedRuns}
+ .selectedAttempts=${this.selectedAttempts}
+ @run-selected=${this.handleRunSelected}
></gr-checks-results>
</div>
`;
diff --git a/polygerrit-ui/app/elements/checks/gr-diff-check-result.ts b/polygerrit-ui/app/elements/checks/gr-diff-check-result.ts
index b42c4fd..a602c72 100644
--- a/polygerrit-ui/app/elements/checks/gr-diff-check-result.ts
+++ b/polygerrit-ui/app/elements/checks/gr-diff-check-result.ts
@@ -129,19 +129,19 @@
const cat = this.result.category.toLowerCase();
return html`
<div class="${cat} container font-normal">
- <div class="header" @click="${this.toggleExpandedClick}">
+ <div class="header" @click=${this.toggleExpandedClick}>
<div class="icon">
<iron-icon
icon="gr-icons:${iconFor(this.result.category)}"
></iron-icon>
</div>
<div class="name">
- <gr-hovercard-run .run="${this.result}"></gr-hovercard-run>
+ <gr-hovercard-run .run=${this.result}></gr-hovercard-run>
<div
class="name"
role="button"
tabindex="0"
- @keydown="${this.toggleExpandedPress}"
+ @keydown=${this.toggleExpandedPress}
>
${this.result.checkName}
</div>
@@ -166,16 +166,16 @@
class="show-hide"
role="switch"
tabindex="0"
- aria-checked="${this.isExpanded ? 'true' : 'false'}"
- aria-label="${this.isExpanded
+ aria-checked=${this.isExpanded ? 'true' : 'false'}
+ aria-label=${this.isExpanded
? 'Collapse result row'
- : 'Expand result row'}"
- @keydown="${this.toggleExpandedPress}"
+ : 'Expand result row'}
+ @keydown=${this.toggleExpandedPress}
>
<iron-icon
- icon="${this.isExpanded
+ icon=${this.isExpanded
? 'gr-icons:expand-less'
- : 'gr-icons:expand-more'}"
+ : 'gr-icons:expand-more'}
></iron-icon>
</div>
`;
@@ -186,7 +186,7 @@
return html`
<gr-result-expanded
hidecodepointers
- .result="${this.result}"
+ .result=${this.result}
></gr-result-expanded>
`;
}
diff --git a/polygerrit-ui/app/elements/checks/gr-hovercard-run.ts b/polygerrit-ui/app/elements/checks/gr-hovercard-run.ts
index ecc24c4..cc38693 100644
--- a/polygerrit-ui/app/elements/checks/gr-hovercard-run.ts
+++ b/polygerrit-ui/app/elements/checks/gr-hovercard-run.ts
@@ -137,7 +137,7 @@
<div id="container" role="tooltip" tabindex="-1">
<div class="section">
<div
- ?hidden="${!this.run || this.run.status === RunStatus.RUNNABLE}"
+ ?hidden=${!this.run || this.run.status === RunStatus.RUNNABLE}
class="chipRow"
>
<div class="chip">
@@ -147,8 +147,8 @@
</div>
</div>
<div class="section">
- <div class="sectionIcon" ?hidden="${icon.length === 0}">
- <iron-icon class="${icon}" icon="gr-icons:${icon}"></iron-icon>
+ <div class="sectionIcon" ?hidden=${icon.length === 0}>
+ <iron-icon class=${icon} icon="gr-icons:${icon}"></iron-icon>
</div>
<div class="sectionContent">
<h3 class="name heading-3">
@@ -177,7 +177,7 @@
? html` <div class="row">
<div class="title">Status</div>
<div>
- <a href="${this.run.statusLink}" target="_blank"
+ <a href=${this.run.statusLink} target="_blank"
><iron-icon
aria-label="external link to check status"
class="small link"
@@ -222,7 +222,7 @@
<div>
<div class="attemptIcon">
<iron-icon
- class="${attempt.icon}"
+ class=${attempt.icon}
icon="gr-icons:${attempt.icon}"
></iron-icon>
</div>
@@ -320,7 +320,7 @@
? html` <div class="row">
<div class="title">Documentation</div>
<div>
- <a href="${this.run.checkLink}" target="_blank"
+ <a href=${this.run.checkLink} target="_blank"
><iron-icon
aria-label="external link to check documentation"
class="small link"
@@ -344,8 +344,8 @@
<div class="action">
<gr-checks-action
context="hovercard"
- .eventTarget="${this._target}"
- .action="${action}"
+ .eventTarget=${this._target}
+ .action=${action}
></gr-checks-action>
</div>
`
diff --git a/polygerrit-ui/app/elements/core/gr-account-dropdown/gr-account-dropdown.ts b/polygerrit-ui/app/elements/core/gr-account-dropdown/gr-account-dropdown.ts
index b55d5a4..fbe1c60 100644
--- a/polygerrit-ui/app/elements/core/gr-account-dropdown/gr-account-dropdown.ts
+++ b/polygerrit-ui/app/elements/core/gr-account-dropdown/gr-account-dropdown.ts
@@ -97,16 +97,16 @@
override render() {
return html`<gr-dropdown
link=""
- .items="${this.links}"
- .topContent="${this.topContent}"
+ .items=${this.links}
+ .topContent=${this.topContent}
@tap-item-shortcuts=${this._handleShortcutsTap}
.horizontalAlign=${'right'}
>
- <span ?hidden="${this._hasAvatars}"
+ <span ?hidden=${this._hasAvatars}
>${this._accountName(this.account)}</span
>
<gr-avatar
- .account="${this.account}"
+ .account=${this.account}
?hidden=${!this._hasAvatars}
.imageSize=${56}
aria-label="Account avatar"
diff --git a/polygerrit-ui/app/elements/core/gr-error-dialog/gr-error-dialog.ts b/polygerrit-ui/app/elements/core/gr-error-dialog/gr-error-dialog.ts
index ba0984b..a0f286f 100644
--- a/polygerrit-ui/app/elements/core/gr-error-dialog/gr-error-dialog.ts
+++ b/polygerrit-ui/app/elements/core/gr-error-dialog/gr-error-dialog.ts
@@ -88,7 +88,7 @@
return html`
<gr-button id="signIn" class="signInLink" link="" slot="footer">
- <a class="signInLink" href="${this.loginUrl}">Sign in</a>
+ <a class="signInLink" href=${this.loginUrl}>Sign in</a>
</gr-button>
`;
}
diff --git a/polygerrit-ui/app/elements/core/gr-error-manager/gr-error-manager.ts b/polygerrit-ui/app/elements/core/gr-error-manager/gr-error-manager.ts
index 54c8b89..d777c16 100644
--- a/polygerrit-ui/app/elements/core/gr-error-manager/gr-error-manager.ts
+++ b/polygerrit-ui/app/elements/core/gr-error-manager/gr-error-manager.ts
@@ -19,12 +19,9 @@
import '../gr-error-dialog/gr-error-dialog';
import '../../shared/gr-alert/gr-alert';
import '../../shared/gr-overlay/gr-overlay';
-import {PolymerElement} from '@polymer/polymer/polymer-element';
-import {htmlTemplate} from './gr-error-manager_html';
import {getBaseUrl} from '../../../utils/url-util';
import {getAppContext} from '../../../services/app-context';
import {IronA11yAnnouncer} from '@polymer/iron-a11y-announcer/iron-a11y-announcer';
-import {customElement, property} from '@polymer/decorators';
import {GrOverlay} from '../../shared/gr-overlay/gr-overlay';
import {GrErrorDialog} from '../gr-error-dialog/gr-error-dialog';
import {GrAlert} from '../../shared/gr-alert/gr-alert';
@@ -40,6 +37,8 @@
import {windowLocationReload} from '../../../utils/dom-util';
import {debounce, DelayedTask} from '../../../utils/async-util';
import {fireIronAnnounce} from '../../../utils/event-util';
+import {LitElement, html} from 'lit';
+import {customElement, property, query, state} from 'lit/decorators';
const HIDE_ALERT_TIMEOUT_MS = 10 * 1000;
const CHECK_SIGN_IN_INTERVAL_MS = 60 * 1000;
@@ -67,14 +66,6 @@
export const __testOnly_ErrorType = ErrorType;
-export interface GrErrorManager {
- $: {
- noInteractionOverlay: GrOverlay;
- errorDialog: GrErrorDialog;
- errorOverlay: GrOverlay;
- };
-}
-
export function constructServerErrorMsg({
errorText,
status,
@@ -107,32 +98,29 @@
}
@customElement('gr-error-manager')
-export class GrErrorManager extends PolymerElement {
- static get template() {
- return htmlTemplate;
- }
-
+export class GrErrorManager extends LitElement {
/**
* The ID of the account that was logged in when the app was launched. If
* not set, then there was no account at launch.
*/
- @property({type: Number})
- knownAccountId?: AccountId | null;
+ @state() knownAccountId?: AccountId | null;
- @property({type: Object})
- _alertElement: GrAlert | null = null;
+ @state() alertElement: GrAlert | null = null;
- @property({type: Number})
- _hideAlertHandle: number | null = null;
+ @state() hideAlertHandle: number | null = null;
- @property({type: Boolean})
- _refreshingCredentials = false;
+ @state() refreshingCredentials = false;
+
+ @query('#noInteractionOverlay') noInteractionOverlay!: GrOverlay;
+
+ @query('#errorDialog') errorDialog!: GrErrorDialog;
+
+ @query('#errorOverlay') errorOverlay!: GrOverlay;
/**
* The time (in milliseconds) since the most recent credential check.
*/
- @property({type: Number})
- _lastCredentialCheck: number = Date.now();
+ @state() lastCredentialCheck: number = Date.now();
@property({type: String})
loginUrl = '/login';
@@ -143,7 +131,7 @@
private readonly eventEmitter = getAppContext().eventEmitter;
- _authErrorHandlerDeregistrationHook?: Function;
+ private authErrorHandlerDeregistrationHook?: Function;
private readonly restApiService = getAppContext().restApiService;
@@ -159,10 +147,10 @@
document.addEventListener('visibilitychange', this.handleVisibilityChange);
document.addEventListener('show-auth-required', this.handleAuthRequired);
- this._authErrorHandlerDeregistrationHook = this.eventEmitter.on(
+ this.authErrorHandlerDeregistrationHook = this.eventEmitter.on(
'auth-error',
event => {
- this._handleAuthError(event.message, event.action);
+ this.handleAuthError(event.message, event.action);
}
);
@@ -172,7 +160,7 @@
}
override disconnectedCallback() {
- this._clearHideAlertHandle();
+ this.clearHideAlertHandle();
document.removeEventListener(
EventType.SERVER_ERROR,
this.handleServerError
@@ -191,26 +179,46 @@
document.removeEventListener('show-auth-required', this.handleAuthRequired);
this.checkLoggedInTask?.cancel();
- if (this._authErrorHandlerDeregistrationHook) {
- this._authErrorHandlerDeregistrationHook();
+ if (this.authErrorHandlerDeregistrationHook) {
+ this.authErrorHandlerDeregistrationHook();
}
super.disconnectedCallback();
}
- _shouldSuppressError(msg: string) {
+ override render() {
+ return html`
+ <gr-overlay with-backdrop="" id="errorOverlay">
+ <gr-error-dialog
+ id="errorDialog"
+ @dismiss=${() => this.errorOverlay.close()}
+ .loginUrl=${this.loginUrl}
+ ></gr-error-dialog>
+ </gr-overlay>
+ <gr-overlay
+ id="noInteractionOverlay"
+ with-backdrop=""
+ always-on-top=""
+ no-cancel-on-esc-key=""
+ no-cancel-on-outside-click=""
+ >
+ </gr-overlay>
+ `;
+ }
+
+ private shouldSuppressError(msg: string) {
return msg.includes(TOO_MANY_FILES);
}
private readonly handleAuthRequired = () => {
- this._showAuthErrorAlert(
+ this.showAuthErrorAlert(
'Log in is required to perform that action.',
'Log in.'
);
};
- _handleAuthError(msg: string, action: string) {
- this.$.noInteractionOverlay.open().then(() => {
- this._showAuthErrorAlert(msg, action);
+ private handleAuthError(msg: string, action: string) {
+ this.noInteractionOverlay.open().then(() => {
+ this.showAuthErrorAlert(msg, action);
});
}
@@ -237,11 +245,11 @@
// Re-check on auth
this._authService.clearCache();
this.restApiService.getLoggedIn();
- } else if (!this._shouldSuppressError(errorText)) {
+ } else if (!this.shouldSuppressError(errorText)) {
const trace =
response.headers && response.headers.get('X-Gerrit-Trace');
if (response.status === 404) {
- this._showNotFoundMessageWithTip({
+ this.showNotFoundMessageWithTip({
status,
statusText,
errorText,
@@ -249,9 +257,9 @@
trace,
});
} else if (response.status === 429) {
- this._showQuotaExceeded({status, statusText});
+ this.showQuotaExceeded({status, statusText});
} else {
- this._showErrorDialog(
+ this.showErrorDialog(
constructServerErrorMsg({
status,
statusText,
@@ -266,7 +274,7 @@
});
};
- _showNotFoundMessageWithTip({
+ private showNotFoundMessageWithTip({
status,
statusText,
errorText,
@@ -277,7 +285,7 @@
const tip = isLoggedIn
? 'You might have not enough privileges.'
: 'You might have not enough privileges. Sign in and try again.';
- this._showErrorDialog(
+ this.showErrorDialog(
constructServerErrorMsg({
status,
statusText,
@@ -293,10 +301,10 @@
});
}
- _showQuotaExceeded({status, statusText}: ErrorMsg) {
+ private showQuotaExceeded({status, statusText}: ErrorMsg) {
const tip = 'Try again later';
const errorText = 'Too many requests from this client';
- this._showErrorDialog(
+ this.showErrorDialog(
constructServerErrorMsg({
status,
statusText,
@@ -324,7 +332,8 @@
// TODO(dhruvsr): allow less priority alerts to override high priority alerts
// In some use cases we may want generic alerts to show along/over errors
- _canOverride(incoming = ErrorType.GENERIC, existing = ErrorType.GENERIC) {
+ // private but used in tests
+ canOverride(incoming = ErrorType.GENERIC, existing = ErrorType.GENERIC) {
return ErrorTypePriority[incoming] >= ErrorTypePriority[existing];
}
@@ -336,70 +345,72 @@
type?: ErrorType,
showDismiss?: boolean
) {
- if (this._alertElement) {
+ if (this.alertElement) {
// check priority before hiding
- if (!this._canOverride(type, this._alertElement.type)) return;
+ if (!this.canOverride(type, this.alertElement.type)) return;
this.hideAlert();
}
- this._clearHideAlertHandle();
+ this.clearHideAlertHandle();
if (dismissOnNavigation) {
// Persist alert until navigation.
document.addEventListener('location-change', this.hideAlert);
} else {
- this._hideAlertHandle = window.setTimeout(
+ this.hideAlertHandle = window.setTimeout(
this.hideAlert,
HIDE_ALERT_TIMEOUT_MS
);
}
- const el = this._createToastAlert(showDismiss);
+ const el = this.createToastAlert(showDismiss);
el.show(text, actionText, actionCallback);
- this._alertElement = el;
+ this.alertElement = el;
fireIronAnnounce(this, `Alert: ${text}`);
this.reporting.reportInteraction('show-alert', {text});
}
private readonly hideAlert = () => {
- if (!this._alertElement) {
+ if (!this.alertElement) {
return;
}
- this._alertElement.hide();
- this._alertElement = null;
+ this.alertElement.hide();
+ this.alertElement = null;
// Remove listener for page navigation, if it exists.
document.removeEventListener('location-change', this.hideAlert);
};
- _clearHideAlertHandle() {
- if (this._hideAlertHandle !== null) {
- window.clearTimeout(this._hideAlertHandle);
- this._hideAlertHandle = null;
+ private clearHideAlertHandle() {
+ if (this.hideAlertHandle !== null) {
+ window.clearTimeout(this.hideAlertHandle);
+ this.hideAlertHandle = null;
}
}
- _showAuthErrorAlert(errorText: string, actionText?: string) {
+ // private but used in tests
+ showAuthErrorAlert(errorText: string, actionText?: string) {
// hide any existing alert like `reload`
// as auth error should have the highest priority
- if (this._alertElement) {
- this._alertElement.hide();
+ if (this.alertElement) {
+ this.alertElement.hide();
}
- this._alertElement = this._createToastAlert();
- this._alertElement.type = ErrorType.AUTH;
- this._alertElement.show(errorText, actionText, () =>
- this._createLoginPopup()
+ this.alertElement = this.createToastAlert();
+ this.alertElement.type = ErrorType.AUTH;
+ this.alertElement.show(errorText, actionText, () =>
+ this.createLoginPopup()
);
fireIronAnnounce(this, errorText);
this.reporting.reportInteraction('show-auth-error', {text: errorText});
- this._refreshingCredentials = true;
- this._requestCheckLoggedIn();
+ this.refreshingCredentials = true;
+ this.requestCheckLoggedIn();
if (!document.hidden) {
this.handleVisibilityChange();
}
}
- _createToastAlert(showDismiss?: boolean) {
+ // private but used in tests
+ createToastAlert(showDismiss?: boolean) {
const el = document.createElement('gr-alert');
el.toast = true;
el.showDismiss = !!showDismiss;
@@ -413,49 +424,51 @@
// If not currently refreshing credentials and the credentials are old,
// request them to confirm their validity or (display an auth toast if it
// fails).
- const timeSinceLastCheck = Date.now() - this._lastCredentialCheck;
+ const timeSinceLastCheck = Date.now() - this.lastCredentialCheck;
if (
- !this._refreshingCredentials &&
+ !this.refreshingCredentials &&
this.knownAccountId !== undefined &&
timeSinceLastCheck > STALE_CREDENTIAL_THRESHOLD_MS
) {
this.reporting.reportInteraction('visibility-sign-in-check');
- this._lastCredentialCheck = Date.now();
+ this.lastCredentialCheck = Date.now();
// check auth status in case:
// - user signed out
// - user switched account
- this._checkSignedIn();
+ this.checkSignedIn();
}
};
- _requestCheckLoggedIn() {
+ // private but used in tests
+ requestCheckLoggedIn() {
this.checkLoggedInTask = debounce(
this.checkLoggedInTask,
- () => this._checkSignedIn(),
+ () => this.checkSignedIn(),
CHECK_SIGN_IN_INTERVAL_MS
);
}
- _checkSignedIn() {
- this._lastCredentialCheck = Date.now();
+ // private but used in tests
+ checkSignedIn() {
+ this.lastCredentialCheck = Date.now();
// force to refetch account info
this.restApiService.invalidateAccountsCache();
this._authService.clearCache();
this.restApiService.getLoggedIn().then(isLoggedIn => {
- if (!this._refreshingCredentials) return;
+ if (!this.refreshingCredentials) return;
if (!isLoggedIn) {
// check later
// 1. guest mode
// 2. or signed out
// in case #2, auth-error is taken care of separately
- this._requestCheckLoggedIn();
+ this.requestCheckLoggedIn();
} else {
this.restApiService.getAccount().then(account => {
- if (this._refreshingCredentials) {
+ if (this.refreshingCredentials) {
// If the credentials were refreshed but the account is different,
// then reload the page completely.
if (account?._account_id !== this.knownAccountId) {
@@ -463,7 +476,7 @@
oldAccount: !!this.knownAccountId,
newAccount: !!account?._account_id,
});
- this._reloadPage();
+ this.reloadPage();
return;
}
@@ -474,11 +487,11 @@
});
}
- _reloadPage() {
+ reloadPage() {
windowLocationReload();
}
- _createLoginPopup() {
+ private createLoginPopup() {
const left = window.screenLeft + (window.outerWidth - SIGN_IN_WIDTH_PX) / 2;
const top = window.screenTop + (window.outerHeight - SIGN_IN_HEIGHT_PX) / 2;
const options = [
@@ -495,12 +508,13 @@
window.addEventListener('focus', this.handleWindowFocus);
}
+ // private but used in tests
handleCredentialRefreshed() {
window.removeEventListener('focus', this.handleWindowFocus);
- this._refreshingCredentials = false;
+ this.refreshingCredentials = false;
this.hideAlert();
this._showAlert('Credentials refreshed.');
- this.$.noInteractionOverlay.close();
+ this.noInteractionOverlay.close();
// Clear the cache for auth
this._authService.clearCache();
@@ -511,19 +525,15 @@
};
private readonly handleShowErrorDialog = (e: ShowErrorEvent) => {
- this._showErrorDialog(e.detail.message);
+ this.showErrorDialog(e.detail.message);
};
- _handleDismissErrorDialog() {
- this.$.errorOverlay.close();
- }
-
- _showErrorDialog(message: string, options?: {showSignInButton?: boolean}) {
+ // private but used in tests
+ showErrorDialog(message: string, options?: {showSignInButton?: boolean}) {
this.reporting.reportErrorDialog(message);
- this.$.errorDialog.text = message;
- this.$.errorDialog.showSignInButton =
- !!options && !!options.showSignInButton;
- this.$.errorOverlay.open();
+ this.errorDialog.text = message;
+ this.errorDialog.showSignInButton = !!options && !!options.showSignInButton;
+ this.errorOverlay.open();
}
}
diff --git a/polygerrit-ui/app/elements/core/gr-error-manager/gr-error-manager_html.ts b/polygerrit-ui/app/elements/core/gr-error-manager/gr-error-manager_html.ts
deleted file mode 100644
index c67ed07..0000000
--- a/polygerrit-ui/app/elements/core/gr-error-manager/gr-error-manager_html.ts
+++ /dev/null
@@ -1,37 +0,0 @@
-/**
- * @license
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-import {html} from '@polymer/polymer/lib/utils/html-tag';
-
-export const htmlTemplate = html`
- <gr-overlay with-backdrop="" id="errorOverlay">
- <gr-error-dialog
- id="errorDialog"
- on-dismiss="_handleDismissErrorDialog"
- confirm-label="Dismiss"
- confirm-on-enter=""
- login-url="[[loginUrl]]"
- ></gr-error-dialog>
- </gr-overlay>
- <gr-overlay
- id="noInteractionOverlay"
- with-backdrop=""
- always-on-top=""
- no-cancel-on-esc-key=""
- no-cancel-on-outside-click=""
- >
- </gr-overlay>
-`;
diff --git a/polygerrit-ui/app/elements/core/gr-error-manager/gr-error-manager_test.ts b/polygerrit-ui/app/elements/core/gr-error-manager/gr-error-manager_test.ts
index d81be15..79321e1 100644
--- a/polygerrit-ui/app/elements/core/gr-error-manager/gr-error-manager_test.ts
+++ b/polygerrit-ui/app/elements/core/gr-error-manager/gr-error-manager_test.ts
@@ -31,8 +31,8 @@
import {tap} from '@polymer/iron-test-helpers/mock-interactions';
import {AccountId} from '../../../types/common';
import {waitUntil} from '../../../test/test-utils';
-
-const basicFixture = fixtureFromElement('gr-error-manager');
+import {fixture} from '@open-wc/testing-helpers';
+import {html} from 'lit';
suite('gr-error-manager tests', () => {
let element: GrErrorManager;
@@ -43,7 +43,7 @@
let getLoggedInStub: sinon.SinonStub;
let appContext: AppContext;
- setup(() => {
+ setup(async () => {
fetchStub = stubAuth('fetch').returns(
Promise.resolve({...new Response(), ok: true, status: 204})
);
@@ -54,9 +54,12 @@
stubRestApi('getPreferences').returns(
Promise.resolve(createPreferences())
);
- element = basicFixture.instantiate();
+ element = await fixture<GrErrorManager>(
+ html`<gr-error-manager></gr-error-manager>`
+ );
appContext.authService.clearCache();
- toastSpy = sinon.spy(element, '_createToastAlert');
+ toastSpy = sinon.spy(element, 'createToastAlert');
+ await element.updateComplete;
});
teardown(() => {
@@ -66,7 +69,7 @@
});
test('does not show auth error on 403 by default', async () => {
- const showAuthErrorStub = sinon.stub(element, '_showAuthErrorAlert');
+ const showAuthErrorStub = sinon.stub(element, 'showAuthErrorAlert');
const responseText = Promise.resolve('server says no.');
element.dispatchEvent(
new CustomEvent('server-error', {
@@ -87,7 +90,7 @@
});
test('show auth required for 403 with auth error and not authed before', async () => {
- const showAuthErrorStub = sinon.stub(element, '_showAuthErrorAlert');
+ const showAuthErrorStub = sinon.stub(element, 'showAuthErrorAlert');
const responseText = Promise.resolve('Authentication required\n');
getLoggedInStub.returns(Promise.resolve(true));
element.dispatchEvent(
@@ -132,7 +135,7 @@
});
test('show logged in error', () => {
- const spy = sinon.spy(element, '_showAuthErrorAlert');
+ const spy = sinon.spy(element, 'showAuthErrorAlert');
element.dispatchEvent(
new CustomEvent('show-auth-required', {
composed: true,
@@ -148,7 +151,7 @@
});
test('show normal Error', async () => {
- const showErrorSpy = sinon.spy(element, '_showErrorDialog');
+ const showErrorSpy = sinon.spy(element, 'showErrorDialog');
const textSpy = sinon.spy(() => Promise.resolve('ZOMG'));
element.dispatchEvent(
new CustomEvent('server-error', {
@@ -219,10 +222,7 @@
})
);
await flush();
- assert.equal(
- element.$.errorDialog.text,
- 'Error 500: 500\nTrace Id: xxxx'
- );
+ assert.equal(element.errorDialog.text, 'Error 500: 500\nTrace Id: xxxx');
});
test('suppress TOO_MANY_FILES error', async () => {
@@ -259,31 +259,29 @@
);
});
- test('_canOverride alerts', () => {
+ test('canOverride alerts', () => {
+ assert.isFalse(element.canOverride(undefined, __testOnly_ErrorType.AUTH));
assert.isFalse(
- element._canOverride(undefined, __testOnly_ErrorType.AUTH)
- );
- assert.isFalse(
- element._canOverride(undefined, __testOnly_ErrorType.NETWORK)
+ element.canOverride(undefined, __testOnly_ErrorType.NETWORK)
);
assert.isTrue(
- element._canOverride(undefined, __testOnly_ErrorType.GENERIC)
+ element.canOverride(undefined, __testOnly_ErrorType.GENERIC)
);
- assert.isTrue(element._canOverride(undefined, undefined));
+ assert.isTrue(element.canOverride(undefined, undefined));
assert.isTrue(
- element._canOverride(__testOnly_ErrorType.NETWORK, undefined)
+ element.canOverride(__testOnly_ErrorType.NETWORK, undefined)
);
- assert.isTrue(element._canOverride(__testOnly_ErrorType.AUTH, undefined));
+ assert.isTrue(element.canOverride(__testOnly_ErrorType.AUTH, undefined));
assert.isFalse(
- element._canOverride(
+ element.canOverride(
__testOnly_ErrorType.NETWORK,
__testOnly_ErrorType.AUTH
)
);
assert.isTrue(
- element._canOverride(
+ element.canOverride(
__testOnly_ErrorType.AUTH,
__testOnly_ErrorType.NETWORK
)
@@ -336,7 +334,7 @@
assert.include(toast.shadowRoot.textContent, 'Refresh credentials');
// noInteractionOverlay
- const noInteractionOverlay = element.$.noInteractionOverlay;
+ const noInteractionOverlay = element.noInteractionOverlay;
assert.isOk(noInteractionOverlay);
const noInteractionOverlayCloseSpy = sinon.spy(
noInteractionOverlay,
@@ -360,7 +358,7 @@
clock.tick(1000);
element.knownAccountId = 5 as AccountId;
- element._checkSignedIn();
+ element.checkSignedIn();
await flush();
assert.isTrue(refreshStub.called);
@@ -525,15 +523,15 @@
});
test('checks stale credentials on visibility change', () => {
- const refreshStub = sinon.stub(element, '_checkSignedIn');
+ const refreshStub = sinon.stub(element, 'checkSignedIn');
sinon.stub(Date, 'now').returns(999999);
- element._lastCredentialCheck = 0;
+ element.lastCredentialCheck = 0;
document.dispatchEvent(new CustomEvent('visibilitychange'));
// Since there is no known account, it should not test credentials.
assert.isFalse(refreshStub.called);
- assert.equal(element._lastCredentialCheck, 0);
+ assert.equal(element.lastCredentialCheck, 0);
element.knownAccountId = 123 as AccountId;
@@ -541,7 +539,7 @@
// Should test credentials, since there is a known account.
assert.isTrue(refreshStub.called);
- assert.equal(element._lastCredentialCheck, 999999);
+ assert.equal(element.lastCredentialCheck, 999999);
});
test('refreshes with same credentials', async () => {
@@ -549,16 +547,16 @@
...createAccountDetailWithId(1234),
});
stubRestApi('getAccount').returns(accountPromise);
- const requestCheckStub = sinon.stub(element, '_requestCheckLoggedIn');
+ const requestCheckStub = sinon.stub(element, 'requestCheckLoggedIn');
const handleRefreshStub = sinon.stub(
element,
'handleCredentialRefreshed'
);
- const reloadStub = sinon.stub(element, '_reloadPage');
+ const reloadStub = sinon.stub(element, 'reloadPage');
element.knownAccountId = 1234 as AccountId;
- element._refreshingCredentials = true;
- element._checkSignedIn();
+ element.refreshingCredentials = true;
+ element.checkSignedIn();
await flush();
assert.isFalse(requestCheckStub.called);
@@ -567,15 +565,15 @@
});
test('_showAlert hides existing alerts', () => {
- element._alertElement = element._createToastAlert();
+ element.alertElement = element.createToastAlert();
// const hideStub = sinon.stub(element, 'hideAlert');
// element._showAlert('');
// assert.isTrue(hideStub.calledOnce);
});
test('show-error', async () => {
- const openStub = sinon.stub(element.$.errorOverlay, 'open');
- const closeStub = sinon.stub(element.$.errorOverlay, 'close');
+ const openStub = sinon.stub(element.errorOverlay, 'open');
+ const closeStub = sinon.stub(element.errorOverlay, 'close');
const reportStub = stubReporting('reportErrorDialog');
const message = 'test message';
@@ -590,9 +588,9 @@
assert.isTrue(openStub.called);
assert.isTrue(reportStub.called);
- assert.equal(element.$.errorDialog.text, message);
+ assert.equal(element.errorDialog.text, message);
- element.$.errorDialog.dispatchEvent(
+ element.errorDialog.dispatchEvent(
new CustomEvent('dismiss', {
composed: true,
bubbles: true,
@@ -608,16 +606,16 @@
...createAccountDetailWithId(1234),
});
stubRestApi('getAccount').returns(accountPromise);
- const requestCheckStub = sinon.stub(element, '_requestCheckLoggedIn');
+ const requestCheckStub = sinon.stub(element, 'requestCheckLoggedIn');
const handleRefreshStub = sinon.stub(
element,
'handleCredentialRefreshed'
);
- const reloadStub = sinon.stub(element, '_reloadPage');
+ const reloadStub = sinon.stub(element, 'reloadPage');
element.knownAccountId = 4321 as AccountId; // Different from 1234
- element._refreshingCredentials = true;
- element._checkSignedIn();
+ element.refreshingCredentials = true;
+ element.checkSignedIn();
await flush();
@@ -629,10 +627,13 @@
suite('when not authed', () => {
let toastSpy: sinon.SinonSpy;
- setup(() => {
+ setup(async () => {
stubRestApi('getLoggedIn').returns(Promise.resolve(false));
- element = basicFixture.instantiate();
- toastSpy = sinon.spy(element, '_createToastAlert');
+ element = await fixture<GrErrorManager>(
+ html`<gr-error-manager></gr-error-manager>`
+ );
+ toastSpy = sinon.spy(element, 'createToastAlert');
+ await element.updateComplete;
});
teardown(() => {
@@ -642,15 +643,15 @@
});
test('refresh loop continues on credential fail', async () => {
- const requestCheckStub = sinon.stub(element, '_requestCheckLoggedIn');
+ const requestCheckStub = sinon.stub(element, 'requestCheckLoggedIn');
const handleRefreshStub = sinon.stub(
element,
'handleCredentialRefreshed'
);
- const reloadStub = sinon.stub(element, '_reloadPage');
+ const reloadStub = sinon.stub(element, 'reloadPage');
- element._refreshingCredentials = true;
- element._checkSignedIn();
+ element.refreshingCredentials = true;
+ element.checkSignedIn();
await flush();
assert.isTrue(requestCheckStub.called);
diff --git a/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header.ts b/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header.ts
index 5cc3737..e736928 100644
--- a/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header.ts
+++ b/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header.ts
@@ -358,7 +358,7 @@
override render() {
return html`
<nav>
- <a href="${`//${window.location.host}${getBaseUrl()}/`}" class="bigTitle">
+ <a href=${`//${window.location.host}${getBaseUrl()}/`} class="bigTitle">
<gr-endpoint-decorator name="header-title">
<span class="titleText"></span>
</gr-endpoint-decorator>
@@ -398,14 +398,14 @@
private renderLinkGroup(linkGroup: MainHeaderLinkGroup) {
return html`
- <li class="${linkGroup.class ?? ''}">
+ <li class=${linkGroup.class ?? ''}>
<gr-dropdown
link
down-arrow
.items=${linkGroup.links}
horizontal-align="left"
>
- <span class="linksTitle" id="${linkGroup.title}">
+ <span class="linksTitle" id=${linkGroup.title}>
${linkGroup.title}
</span>
</gr-dropdown>
@@ -418,7 +418,7 @@
return html`
<a
- href="${this.feedbackURL}"
+ href=${this.feedbackURL}
title="File a bug"
aria-label="File a bug"
target="_blank"
@@ -439,12 +439,12 @@
this.onMobileSearchTap(e);
}}
role="button"
- aria-label="${this.mobileSearchHidden
+ aria-label=${this.mobileSearchHidden
? 'Show Searchbar'
- : 'Hide Searchbar'}"
+ : 'Hide Searchbar'}
></iron-icon>
${this.renderRegister()}
- <a class="loginButton" href="${this.loginUrl}">Sign in</a>
+ <a class="loginButton" href=${this.loginUrl}>Sign in</a>
<a
class="settingsButton"
href="${getBaseUrl()}/settings/"
@@ -464,7 +464,7 @@
return html`
<div class="registerDiv">
- <a class="registerButton" href="${this.registerURL}">
+ <a class="registerButton" href=${this.registerURL}>
${this.registerText}
</a>
</div>
diff --git a/polygerrit-ui/app/elements/diff/gr-apply-fix-dialog/gr-apply-fix-dialog.ts b/polygerrit-ui/app/elements/diff/gr-apply-fix-dialog/gr-apply-fix-dialog.ts
index 567e1bb..c264978 100644
--- a/polygerrit-ui/app/elements/diff/gr-apply-fix-dialog/gr-apply-fix-dialog.ts
+++ b/polygerrit-ui/app/elements/diff/gr-apply-fix-dialog/gr-apply-fix-dialog.ts
@@ -1,28 +1,14 @@
/**
* @license
- * Copyright (C) 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
+ * Copyright 2019 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
*/
import '@polymer/iron-icon/iron-icon';
import '../../../styles/shared-styles';
import '../../shared/gr-dialog/gr-dialog';
import '../../shared/gr-overlay/gr-overlay';
import '../../../embed/diff/gr-diff/gr-diff';
-import {PolymerElement} from '@polymer/polymer/polymer-element';
-import {htmlTemplate} from './gr-apply-fix-dialog_html';
import {GerritNav} from '../../core/gr-navigation/gr-navigation';
-import {customElement, property} from '@polymer/decorators';
import {
NumericChangeId,
EditPatchSetNum,
@@ -41,13 +27,9 @@
import {DiffLayer, ParsedChangeInfo} from '../../../types/types';
import {GrButton} from '../../shared/gr-button/gr-button';
import {TokenHighlightLayer} from '../../../embed/diff/gr-diff-builder/token-highlight-layer';
-
-export interface GrApplyFixDialog {
- $: {
- applyFixOverlay: GrOverlay;
- nextFix: GrButton;
- };
-}
+import {css, html, LitElement} from 'lit';
+import {customElement, property, query, state} from 'lit/decorators';
+import {sharedStyles} from '../../../styles/shared-styles';
interface FilePreview {
filepath: string;
@@ -55,10 +37,12 @@
}
@customElement('gr-apply-fix-dialog')
-export class GrApplyFixDialog extends PolymerElement {
- static get template() {
- return htmlTemplate;
- }
+export class GrApplyFixDialog extends LitElement {
+ @query('#applyFixOverlay')
+ applyFixOverlay?: GrOverlay;
+
+ @query('#nextFix')
+ nextFix?: GrButton;
@property({type: Object})
prefs?: DiffPreferencesInfo;
@@ -66,52 +50,142 @@
@property({type: Object})
change?: ParsedChangeInfo;
- @property({type: String})
+ @property({type: Number})
changeNum?: NumericChangeId;
- @property({type: Number})
- _patchNum?: PatchSetNum;
+ @state()
+ patchNum?: PatchSetNum;
- @property({type: String})
- _robotId?: RobotId;
+ @state()
+ robotId?: RobotId;
- @property({type: Object})
- _currentFix?: FixSuggestionInfo;
+ @state()
+ currentFix?: FixSuggestionInfo;
- @property({type: Array})
- _currentPreviews: FilePreview[] = [];
+ @state()
+ currentPreviews: FilePreview[] = [];
- @property({type: Array})
- _fixSuggestions?: FixSuggestionInfo[];
+ @state()
+ fixSuggestions?: FixSuggestionInfo[];
- @property({type: Boolean})
- _isApplyFixLoading = false;
+ @state()
+ isApplyFixLoading = false;
- @property({type: Number})
- _selectedFixIdx = 0;
+ @state()
+ selectedFixIdx = 0;
- @property({
- type: Boolean,
- computed:
- '_computeDisableApplyFixButton(_isApplyFixLoading, change, ' +
- '_patchNum)',
- })
- _disableApplyFixButton = false;
-
- @property({type: Array})
+ @state()
layers: DiffLayer[] = [];
- private refitOverlay?: () => void;
-
private readonly restApiService = getAppContext().restApiService;
constructor() {
super();
+ // TODO Get preferences from model.
this.restApiService.getPreferences().then(prefs => {
if (!prefs?.disable_token_highlighting) {
this.layers = [new TokenHighlightLayer(this)];
}
});
+ this.addEventListener('diff-context-expanded', () => {
+ if (this.applyFixOverlay) fireEvent(this.applyFixOverlay, 'iron-resize');
+ });
+ }
+
+ static override styles = [
+ sharedStyles,
+ css`
+ gr-diff {
+ --content-width: 90vw;
+ }
+ .diffContainer {
+ padding: var(--spacing-l) 0;
+ border-bottom: 1px solid var(--border-color);
+ }
+ .file-name {
+ display: block;
+ padding: var(--spacing-s) var(--spacing-l);
+ background-color: var(--background-color-secondary);
+ border-bottom: 1px solid var(--border-color);
+ }
+ gr-button {
+ margin-left: var(--spacing-m);
+ }
+ .fix-picker {
+ display: flex;
+ align-items: center;
+ margin-right: var(--spacing-l);
+ }
+ `,
+ ];
+
+ override render() {
+ return html`
+ <gr-overlay id="applyFixOverlay" with-backdrop="">
+ <gr-dialog
+ id="applyFixDialog"
+ .confirmLabel=${this.isApplyFixLoading ? 'Saving...' : 'Apply Fix'}
+ .confirmTooltip=${this.computeTooltip()}
+ ?disabled=${this.computeDisableApplyFixButton()}
+ @confirm=${this.handleApplyFix}
+ @cancel=${this.onCancel}
+ >
+ ${this.renderHeader()} ${this.renderMain()} ${this.renderFooter()}
+ </gr-dialog>
+ </gr-overlay>
+ `;
+ }
+
+ private renderHeader() {
+ return html`
+ <div slot="header">
+ ${this.robotId ?? ''} - ${this.currentFix?.description ?? ''}
+ </div>
+ `;
+ }
+
+ private renderMain() {
+ const items = this.currentPreviews.map(
+ item => html`
+ <div class="file-name">
+ <span>${item.filepath}</span>
+ </div>
+ <div class="diffContainer">
+ <gr-diff
+ .prefs=${this.overridePartialPrefs()}
+ .path=${item.filepath}
+ .diff=${item.preview}
+ .layers=${this.layers}
+ ></gr-diff>
+ </div>
+ `
+ );
+ return html`<div slot="main">${items}</div>`;
+ }
+
+ private renderFooter() {
+ const id = this.selectedFixIdx;
+ const fixCount = this.fixSuggestions?.length ?? 0;
+ if (fixCount < 2) return;
+ return html`
+ <div slot="footer" class="fix-picker">
+ <span>Suggested fix ${id + 1} of ${fixCount}</span>
+ <gr-button
+ id="prevFix"
+ @click=${this.onPrevFixClick}
+ ?disabled=${id === 0}
+ >
+ <iron-icon icon="gr-icons:chevron-left"></iron-icon>
+ </gr-button>
+ <gr-button
+ id="nextFix"
+ @click=${this.onNextFixClick}
+ ?disabled=${id === fixCount - 1}
+ >
+ <iron-icon icon="gr-icons:chevron-right"></iron-icon>
+ </gr-button>
+ </div>
+ `;
}
/**
@@ -128,183 +202,131 @@
if (!detail.patchNum || !comment || !isRobot(comment)) {
return Promise.resolve();
}
- this._patchNum = detail.patchNum;
- this._fixSuggestions = comment.fix_suggestions;
- this._robotId = comment.robot_id;
- if (!this._fixSuggestions || !this._fixSuggestions.length) {
+ this.patchNum = detail.patchNum;
+ this.fixSuggestions = comment.fix_suggestions;
+ this.robotId = comment.robot_id;
+ if (!this.fixSuggestions || !this.fixSuggestions.length) {
return Promise.resolve();
}
- this._selectedFixIdx = 0;
+ this.selectedFixIdx = 0;
const promises = [];
promises.push(
- this._showSelectedFixSuggestion(this._fixSuggestions[0]),
- this.$.applyFixOverlay.open()
+ this.showSelectedFixSuggestion(this.fixSuggestions[0]),
+ this.applyFixOverlay?.open()
);
return Promise.all(promises).then(() => {
- // ensures gr-overlay repositions overlay in center
- fireEvent(this.$.applyFixOverlay, 'iron-resize');
+ if (this.applyFixOverlay) fireEvent(this.applyFixOverlay, 'iron-resize');
});
}
- override connectedCallback() {
- super.connectedCallback();
- this.refitOverlay = () => {
- // re-center the dialog as content changed
- fireEvent(this.$.applyFixOverlay, 'iron-resize');
- };
- this.addEventListener('diff-context-expanded', this.refitOverlay);
+ private showSelectedFixSuggestion(fixSuggestion: FixSuggestionInfo) {
+ this.currentFix = fixSuggestion;
+ return this.fetchFixPreview(fixSuggestion.fix_id);
}
- override disconnectedCallback() {
- if (this.refitOverlay) {
- this.removeEventListener('diff-context-expanded', this.refitOverlay);
- }
- super.disconnectedCallback();
- }
-
- _showSelectedFixSuggestion(fixSuggestion: FixSuggestionInfo) {
- this._currentFix = fixSuggestion;
- return this._fetchFixPreview(fixSuggestion.fix_id);
- }
-
- _fetchFixPreview(fixId: FixId) {
- if (!this.changeNum || !this._patchNum) {
+ private fetchFixPreview(fixId: FixId) {
+ if (!this.changeNum || !this.patchNum) {
return Promise.reject(
- new Error('Both _patchNum and changeNum must be set')
+ new Error('Both patchNum and changeNum must be set')
);
}
return this.restApiService
- .getRobotCommentFixPreview(this.changeNum, this._patchNum, fixId)
+ .getRobotCommentFixPreview(this.changeNum, this.patchNum, fixId)
.then(res => {
if (res) {
- this._currentPreviews = Object.keys(res).map(key => {
+ this.currentPreviews = Object.keys(res).map(key => {
return {filepath: key, preview: res[key]};
});
}
})
.catch(err => {
- this._close(false);
+ this.close(false);
throw err;
});
}
- hasSingleFix(_fixSuggestions?: FixSuggestionInfo[]) {
- return (_fixSuggestions || []).length === 1;
- }
-
- overridePartialPrefs(prefs?: DiffPreferencesInfo) {
- if (!prefs) return undefined;
+ private overridePartialPrefs() {
+ if (!this.prefs) return undefined;
// generate a smaller gr-diff than fullscreen for dialog
- return {...prefs, line_length: 50};
+ return {...this.prefs, line_length: 50};
}
+ // visible for testing
onCancel(e: Event) {
- if (e) {
- e.stopPropagation();
- }
- this._close(false);
- }
-
- addOneTo(_selectedFixIdx: number) {
- return _selectedFixIdx + 1;
- }
-
- _onPrevFixClick(e: Event) {
if (e) e.stopPropagation();
- if (this._selectedFixIdx >= 1 && this._fixSuggestions) {
- this._selectedFixIdx -= 1;
- this._showSelectedFixSuggestion(
- this._fixSuggestions[this._selectedFixIdx]
- );
+ this.close(false);
+ }
+
+ // visible for testing
+ onPrevFixClick(e: Event) {
+ if (e) e.stopPropagation();
+ if (this.selectedFixIdx >= 1 && this.fixSuggestions) {
+ this.selectedFixIdx -= 1;
+ this.showSelectedFixSuggestion(this.fixSuggestions[this.selectedFixIdx]);
}
}
- _onNextFixClick(e: Event) {
+ // visible for testing
+ onNextFixClick(e: Event) {
if (e) e.stopPropagation();
if (
- this._fixSuggestions &&
- this._selectedFixIdx < this._fixSuggestions.length
+ this.fixSuggestions &&
+ this.selectedFixIdx < this.fixSuggestions.length
) {
- this._selectedFixIdx += 1;
- this._showSelectedFixSuggestion(
- this._fixSuggestions[this._selectedFixIdx]
- );
+ this.selectedFixIdx += 1;
+ this.showSelectedFixSuggestion(this.fixSuggestions[this.selectedFixIdx]);
}
}
- _noPrevFix(_selectedFixIdx: number) {
- return _selectedFixIdx === 0;
- }
-
- _noNextFix(_selectedFixIdx: number, fixSuggestions?: FixSuggestionInfo[]) {
- if (!fixSuggestions) return true;
- return _selectedFixIdx === fixSuggestions.length - 1;
- }
-
- _close(fixApplied: boolean) {
- this._currentFix = undefined;
- this._currentPreviews = [];
- this._isApplyFixLoading = false;
+ private close(fixApplied: boolean) {
+ this.currentFix = undefined;
+ this.currentPreviews = [];
+ this.isApplyFixLoading = false;
fireCloseFixPreview(this, fixApplied);
- this.$.applyFixOverlay.close();
+ this.applyFixOverlay?.close();
}
- _getApplyFixButtonLabel(isLoading: boolean) {
- return isLoading ? 'Saving...' : 'Apply Fix';
- }
-
- _computeTooltip(change?: ParsedChangeInfo, patchNum?: PatchSetNum) {
- if (!change || !patchNum) return '';
- const latestPatchNum = change.revisions[change.current_revision]._number;
- return latestPatchNum !== patchNum
+ private computeTooltip() {
+ if (!this.change || !this.patchNum) return '';
+ const currentPatchNum =
+ this.change.revisions[this.change.current_revision]._number;
+ return currentPatchNum !== this.patchNum
? 'Fix can only be applied to the latest patchset'
: '';
}
- _computeDisableApplyFixButton(
- isApplyFixLoading: boolean,
- change?: ParsedChangeInfo,
- patchNum?: PatchSetNum
- ) {
- if (!change || isApplyFixLoading === undefined || patchNum === undefined) {
- return true;
- }
- const currentPatchNum = change.revisions[change.current_revision]._number;
- if (patchNum !== currentPatchNum) {
- return true;
- }
- return isApplyFixLoading;
+ private computeDisableApplyFixButton() {
+ if (!this.change || !this.patchNum) return true;
+ const currentPatchNum =
+ this.change.revisions[this.change.current_revision]._number;
+ return this.patchNum !== currentPatchNum || this.isApplyFixLoading;
}
- _handleApplyFix(e: Event) {
- if (e) {
- e.stopPropagation();
- }
+ // visible for testing
+ async handleApplyFix(e: Event) {
+ if (e) e.stopPropagation();
const changeNum = this.changeNum;
- const patchNum = this._patchNum;
+ const patchNum = this.patchNum;
const change = this.change;
- if (!changeNum || !patchNum || !change || !this._currentFix) {
- return Promise.reject(new Error('Not all required properties are set.'));
+ if (!changeNum || !patchNum || !change || !this.currentFix) {
+ throw new Error('Not all required properties are set.');
}
- this._isApplyFixLoading = true;
- return this.restApiService
- .applyFixSuggestion(changeNum, patchNum, this._currentFix.fix_id)
- .then(res => {
- if (res && res.ok) {
- GerritNav.navigateToChange(change, {
- patchNum: EditPatchSetNum,
- basePatchNum: patchNum as BasePatchSetNum,
- });
- this._close(true);
- }
- this._isApplyFixLoading = false;
+ this.isApplyFixLoading = true;
+ const res = await this.restApiService.applyFixSuggestion(
+ changeNum,
+ patchNum,
+ this.currentFix.fix_id
+ );
+ if (res && res.ok) {
+ GerritNav.navigateToChange(change, {
+ patchNum: EditPatchSetNum,
+ basePatchNum: patchNum as BasePatchSetNum,
});
- }
-
- getFixDescription(currentFix?: FixSuggestionInfo) {
- return currentFix && currentFix.description ? currentFix.description : '';
+ this.close(true);
+ }
+ this.isApplyFixLoading = false;
}
}
diff --git a/polygerrit-ui/app/elements/diff/gr-apply-fix-dialog/gr-apply-fix-dialog_html.ts b/polygerrit-ui/app/elements/diff/gr-apply-fix-dialog/gr-apply-fix-dialog_html.ts
deleted file mode 100644
index 0f50157..0000000
--- a/polygerrit-ui/app/elements/diff/gr-apply-fix-dialog/gr-apply-fix-dialog_html.ts
+++ /dev/null
@@ -1,94 +0,0 @@
-/**
- * @license
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-import {html} from '@polymer/polymer/lib/utils/html-tag';
-
-export const htmlTemplate = html`
- <style include="shared-styles">
- gr-diff {
- --content-width: 90vw;
- }
- .diffContainer {
- padding: var(--spacing-l) 0;
- border-bottom: 1px solid var(--border-color);
- }
- .file-name {
- display: block;
- padding: var(--spacing-s) var(--spacing-l);
- background-color: var(--background-color-secondary);
- border-bottom: 1px solid var(--border-color);
- }
- gr-button {
- margin-left: var(--spacing-m);
- }
- .fix-picker {
- display: flex;
- align-items: center;
- margin-right: var(--spacing-l);
- }
- </style>
- <gr-overlay id="applyFixOverlay" with-backdrop="">
- <gr-dialog
- id="applyFixDialog"
- on-confirm="_handleApplyFix"
- confirm-label="[[_getApplyFixButtonLabel(_isApplyFixLoading)]]"
- disabled="[[_disableApplyFixButton]]"
- confirm-tooltip="[[_computeTooltip(change, _patchNum)]]"
- on-cancel="onCancel"
- >
- <div slot="header">[[_robotId]] - [[getFixDescription(_currentFix)]]</div>
- <div slot="main">
- <template is="dom-repeat" items="[[_currentPreviews]]">
- <div class="file-name">
- <span>[[item.filepath]]</span>
- </div>
- <div class="diffContainer">
- <gr-diff
- prefs="[[overridePartialPrefs(prefs)]]"
- path="[[item.filepath]]"
- diff="[[item.preview]]"
- layers="[[layers]]"
- ></gr-diff>
- </div>
- </template>
- </div>
- <div
- slot="footer"
- class="fix-picker"
- hidden$="[[hasSingleFix(_fixSuggestions)]]"
- >
- <span
- >Suggested fix [[addOneTo(_selectedFixIdx)]] of
- [[_fixSuggestions.length]]</span
- >
- <gr-button
- id="prevFix"
- on-click="_onPrevFixClick"
- disabled$="[[_noPrevFix(_selectedFixIdx)]]"
- >
- <iron-icon icon="gr-icons:chevron-left"></iron-icon>
- </gr-button>
- <gr-button
- id="nextFix"
- on-click="_onNextFixClick"
- disabled$="[[_noNextFix(_selectedFixIdx, _fixSuggestions)]]"
- >
- <iron-icon icon="gr-icons:chevron-right"></iron-icon>
- </gr-button>
- </div>
- </gr-dialog>
- </gr-overlay>
-`;
diff --git a/polygerrit-ui/app/elements/diff/gr-apply-fix-dialog/gr-apply-fix-dialog_test.ts b/polygerrit-ui/app/elements/diff/gr-apply-fix-dialog/gr-apply-fix-dialog_test.ts
index 9392cb9d1..2c0fe0d 100644
--- a/polygerrit-ui/app/elements/diff/gr-apply-fix-dialog/gr-apply-fix-dialog_test.ts
+++ b/polygerrit-ui/app/elements/diff/gr-apply-fix-dialog/gr-apply-fix-dialog_test.ts
@@ -1,20 +1,8 @@
/**
* @license
- * Copyright (C) 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
+ * Copyright 2019 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
*/
-
import '../../../test/common-test-setup-karma';
import './gr-apply-fix-dialog';
import {GerritNav} from '../../core/gr-navigation/gr-navigation';
@@ -30,6 +18,7 @@
Timestamp,
UrlEncodedCommentId,
} from '../../../types/common';
+import {Comment} from '../../../utils/comment-util';
import {
createFixSuggestionInfo,
createParsedChange,
@@ -44,8 +33,7 @@
OpenFixPreviewEventDetail,
} from '../../../types/events';
import {GrButton} from '../../shared/gr-button/gr-button';
-
-const basicFixture = fixtureFromElement('gr-apply-fix-dialog');
+import {fixture, html} from '@open-wc/testing-helpers';
suite('gr-apply-fix-dialog tests', () => {
let element: GrApplyFixDialog;
@@ -78,15 +66,29 @@
);
}
- setup(() => {
- element = basicFixture.instantiate();
+ async function open(comment: Comment) {
+ await element.open(
+ new CustomEvent<OpenFixPreviewEventDetail>(EventType.OPEN_FIX_PREVIEW, {
+ detail: {
+ patchNum: 2 as PatchSetNum,
+ comment,
+ },
+ })
+ );
+ await element.updateComplete;
+ }
+
+ setup(async () => {
+ element = await fixture<GrApplyFixDialog>(
+ html`<gr-apply-fix-dialog></gr-apply-fix-dialog>`
+ );
const change = {
...createParsedChange(),
revisions: createRevisions(2),
current_revision: getCurrentRevision(1),
};
element.changeNum = change._number;
- element._patchNum = change.revisions[change.current_revision]._number;
+ element.patchNum = change.revisions[change.current_revision]._number;
element.change = change;
element.prefs = {
...createDefaultDiffPrefs(),
@@ -94,6 +96,7 @@
line_length: 100,
tab_size: 4,
};
+ await element.updateComplete;
});
suite('dialog open', () => {
@@ -156,37 +159,22 @@
f2: diffInfo2,
})
);
- sinon.stub(element.$.applyFixOverlay, 'open').returns(Promise.resolve());
+ sinon.stub(element.applyFixOverlay!, 'open').returns(Promise.resolve());
});
test('dialog opens fetch and sets previews', async () => {
- await element.open(
- new CustomEvent<OpenFixPreviewEventDetail>(EventType.OPEN_FIX_PREVIEW, {
- detail: {
- patchNum: 2 as PatchSetNum,
- comment: ROBOT_COMMENT_WITH_TWO_FIXES,
- },
- })
- );
- assert.equal(element._currentFix!.fix_id, 'fix_1');
- assert.equal(element._currentPreviews.length, 2);
- assert.equal(element._robotId, 'robot_1' as RobotId);
+ await open(ROBOT_COMMENT_WITH_TWO_FIXES);
+ assert.equal(element.currentFix!.fix_id, 'fix_1');
+ assert.equal(element.currentPreviews.length, 2);
+ assert.equal(element.robotId, 'robot_1' as RobotId);
const button = getConfirmButton();
assert.isFalse(button.hasAttribute('disabled'));
assert.equal(button.getAttribute('title'), '');
});
test('tooltip is hidden if apply fix is loading', async () => {
- await element.open(
- new CustomEvent<OpenFixPreviewEventDetail>(EventType.OPEN_FIX_PREVIEW, {
- detail: {
- patchNum: 2 as PatchSetNum,
- comment: ROBOT_COMMENT_WITH_TWO_FIXES,
- },
- })
- );
- element._isApplyFixLoading = true;
- await flush();
+ element.isApplyFixLoading = true;
+ await open(ROBOT_COMMENT_WITH_TWO_FIXES);
const button = getConfirmButton();
assert.isTrue(button.hasAttribute('disabled'));
assert.equal(button.getAttribute('title'), '');
@@ -198,15 +186,7 @@
revisions: createRevisions(2),
current_revision: getCurrentRevision(0),
};
- await element.open(
- new CustomEvent<OpenFixPreviewEventDetail>(EventType.OPEN_FIX_PREVIEW, {
- detail: {
- patchNum: 2 as PatchSetNum,
- comment: ROBOT_COMMENT_WITH_ONE_FIX,
- },
- })
- );
- await flush();
+ await open(ROBOT_COMMENT_WITH_TWO_FIXES);
const button = getConfirmButton();
assert.isTrue(button.hasAttribute('disabled'));
assert.equal(
@@ -216,44 +196,63 @@
});
});
+ test('renders', async () => {
+ await open(ROBOT_COMMENT_WITH_TWO_FIXES);
+ expect(element).shadowDom.to.equal(
+ /* HTML */ `
+ <gr-overlay id="applyFixOverlay" tabindex="-1" with-backdrop="">
+ <gr-dialog id="applyFixDialog" role="dialog">
+ <div slot="header">robot_1 - Fix fix_1</div>
+ <div slot="main"></div>
+ <div class="fix-picker" slot="footer">
+ <span>Suggested fix 1 of 2</span>
+ <gr-button
+ aria-disabled="true"
+ disabled=""
+ id="prevFix"
+ role="button"
+ tabindex="-1"
+ >
+ <iron-icon icon="gr-icons:chevron-left"> </iron-icon>
+ </gr-button>
+ <gr-button
+ aria-disabled="false"
+ id="nextFix"
+ role="button"
+ tabindex="0"
+ >
+ <iron-icon icon="gr-icons:chevron-right"> </iron-icon>
+ </gr-button>
+ </div>
+ </gr-dialog>
+ </gr-overlay>
+ `,
+ {ignoreAttributes: ['style']}
+ );
+ });
+
test('next button state updated when suggestions changed', async () => {
stubRestApi('getRobotCommentFixPreview').returns(Promise.resolve({}));
- sinon.stub(element.$.applyFixOverlay, 'open').returns(Promise.resolve());
+ sinon.stub(element.applyFixOverlay!, 'open').returns(Promise.resolve());
- await element.open(
- new CustomEvent<OpenFixPreviewEventDetail>(EventType.OPEN_FIX_PREVIEW, {
- detail: {
- patchNum: 2 as PatchSetNum,
- comment: ROBOT_COMMENT_WITH_ONE_FIX,
- },
- })
- );
- assert.isTrue(element.$.nextFix.disabled);
- await element.open(
- new CustomEvent<OpenFixPreviewEventDetail>(EventType.OPEN_FIX_PREVIEW, {
- detail: {
- patchNum: 2 as PatchSetNum,
- comment: ROBOT_COMMENT_WITH_TWO_FIXES,
- },
- })
- );
- assert.isFalse(element.$.nextFix.disabled);
+ await open(ROBOT_COMMENT_WITH_ONE_FIX);
+ await element.updateComplete;
+ assert.notOk(element.nextFix);
+ await open(ROBOT_COMMENT_WITH_TWO_FIXES);
+ assert.ok(element.nextFix);
+ assert.notOk(element.nextFix!.disabled);
});
test('preview endpoint throws error should reset dialog', async () => {
stubRestApi('getRobotCommentFixPreview').returns(
Promise.reject(new Error('backend error'))
);
- element.open(
- new CustomEvent<OpenFixPreviewEventDetail>(EventType.OPEN_FIX_PREVIEW, {
- detail: {
- patchNum: 2 as PatchSetNum,
- comment: ROBOT_COMMENT_WITH_TWO_FIXES,
- },
- })
- );
- await flush();
- assert.equal(element._currentFix, undefined);
+ try {
+ await open(ROBOT_COMMENT_WITH_TWO_FIXES);
+ } catch (error) {
+ // expected
+ }
+ assert.equal(element.currentFix, undefined);
});
test('apply fix button should call apply, navigate to change view and fire close', async () => {
@@ -261,7 +260,7 @@
Promise.resolve(new Response(null, {status: 200}))
);
const navigateToChangeStub = sinon.stub(GerritNav, 'navigateToChange');
- element._currentFix = createFixSuggestionInfo('123');
+ element.currentFix = createFixSuggestionInfo('123');
const closeFixPreviewEventSpy = sinon.spy();
// Element is recreated after each test, removeEventListener isn't required
@@ -269,7 +268,7 @@
EventType.CLOSE_FIX_PREVIEW,
closeFixPreviewEventSpy
);
- await element._handleApplyFix(new CustomEvent('confirm'));
+ await element.handleApplyFix(new CustomEvent('confirm'));
sinon.assert.calledOnceWithExactly(
applyFixSuggestionStub,
@@ -292,8 +291,8 @@
);
// reset gr-apply-fix-dialog and close
- assert.equal(element._currentFix, undefined);
- assert.equal(element._currentPreviews.length, 0);
+ assert.equal(element.currentFix, undefined);
+ assert.equal(element.currentPreviews.length, 0);
});
test('should not navigate to change view if incorect reponse', async () => {
@@ -301,9 +300,9 @@
Promise.resolve(new Response(null, {status: 500}))
);
const navigateToChangeStub = sinon.stub(GerritNav, 'navigateToChange');
- element._currentFix = createFixSuggestionInfo('fix_123');
+ element.currentFix = createFixSuggestionInfo('fix_123');
- await element._handleApplyFix(new CustomEvent('confirm'));
+ await element.handleApplyFix(new CustomEvent('confirm'));
sinon.assert.calledWithExactly(
applyFixSuggestionStub,
element.change!._number,
@@ -312,24 +311,17 @@
);
assert.isTrue(navigateToChangeStub.notCalled);
- assert.equal(element._isApplyFixLoading, false);
+ assert.equal(element.isApplyFixLoading, false);
});
test('select fix forward and back of multiple suggested fixes', async () => {
- sinon.stub(element.$.applyFixOverlay, 'open').returns(Promise.resolve());
+ sinon.stub(element.applyFixOverlay!, 'open').returns(Promise.resolve());
- await element.open(
- new CustomEvent<OpenFixPreviewEventDetail>(EventType.OPEN_FIX_PREVIEW, {
- detail: {
- patchNum: 2 as PatchSetNum,
- comment: ROBOT_COMMENT_WITH_TWO_FIXES,
- },
- })
- );
- element._onNextFixClick(new CustomEvent('click'));
- assert.equal(element._currentFix!.fix_id, 'fix_2');
- element._onPrevFixClick(new CustomEvent('click'));
- assert.equal(element._currentFix!.fix_id, 'fix_1');
+ await open(ROBOT_COMMENT_WITH_TWO_FIXES);
+ element.onNextFixClick(new CustomEvent('click'));
+ assert.equal(element.currentFix!.fix_id, 'fix_2');
+ element.onPrevFixClick(new CustomEvent('click'));
+ assert.equal(element.currentFix!.fix_id, 'fix_1');
});
test('server-error should throw for failed apply call', async () => {
@@ -337,7 +329,7 @@
Promise.reject(new Error('backend error'))
);
const navigateToChangeStub = sinon.stub(GerritNav, 'navigateToChange');
- element._currentFix = createFixSuggestionInfo('fix_123');
+ element.currentFix = createFixSuggestionInfo('fix_123');
const closeFixPreviewEventSpy = sinon.spy();
// Element is recreated after each test, removeEventListener isn't required
@@ -347,7 +339,7 @@
);
let expectedError;
- await element._handleApplyFix(new CustomEvent('click')).catch(e => {
+ await element.handleApplyFix(new CustomEvent('click')).catch(e => {
expectedError = e;
});
assert.isOk(expectedError);
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host.ts b/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host.ts
index 85f8ce0..f4e5835 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host.ts
+++ b/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host.ts
@@ -312,9 +312,6 @@
'create-comment',
e => this._handleCreateThread(e)
);
- this.addEventListener('normalize-range', event =>
- this._handleNormalizeRange(event)
- );
this.addEventListener('diff-context-expanded', event =>
this._handleDiffContextExpanded(event)
);
@@ -1215,13 +1212,6 @@
return true;
}
- _handleNormalizeRange(event: CustomEvent) {
- this.reporting.reportInteraction('normalize-range', {
- side: event.detail.side,
- lineNum: event.detail.lineNum,
- });
- }
-
_handleDiffContextExpanded(e: CustomEvent<DiffContextExpandedEventDetail>) {
this.reporting.reportInteraction('diff-context-expanded', {
numLines: e.detail.numLines,
diff --git a/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select.ts b/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select.ts
index eeb636f..39fc048 100644
--- a/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select.ts
+++ b/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select.ts
@@ -180,8 +180,8 @@
<span class="patchRange" aria-label="patch range starts with">
<gr-dropdown-list
id="basePatchDropdown"
- .value="${convertToString(this.basePatchNum)}"
- .items="${this.computeBaseDropdownContent()}"
+ .value=${convertToString(this.basePatchNum)}
+ .items=${this.computeBaseDropdownContent()}
@value-change=${this.handlePatchChange}
>
</gr-dropdown-list>
@@ -191,8 +191,8 @@
<span class="patchRange" aria-label="patch range ends with">
<gr-dropdown-list
id="patchNumDropdown"
- .value="${convertToString(this.patchNum)}"
- .items="${this.computePatchDropdownContent()}"
+ .value=${convertToString(this.patchNum)}
+ .items=${this.computePatchDropdownContent()}
@value-change=${this.handlePatchChange}
>
</gr-dropdown-list>
@@ -206,7 +206,7 @@
return html`<span class="filesWeblinks">
${fileLinks.map(
weblink => html`
- <a target="_blank" rel="noopener" href="${ifDefined(weblink.url)}">
+ <a target="_blank" rel="noopener" href=${ifDefined(weblink.url)}>
${weblink.name}
</a>
`
diff --git a/polygerrit-ui/app/elements/documentation/gr-documentation-search/gr-documentation-search.ts b/polygerrit-ui/app/elements/documentation/gr-documentation-search/gr-documentation-search.ts
index e2acdbf..281b7e54 100644
--- a/polygerrit-ui/app/elements/documentation/gr-documentation-search/gr-documentation-search.ts
+++ b/polygerrit-ui/app/elements/documentation/gr-documentation-search/gr-documentation-search.ts
@@ -70,7 +70,7 @@
<td>Loading...</td>
</tr>
</tbody>
- <tbody class="${this.loading ? 'loading' : ''}">
+ <tbody class=${this.loading ? 'loading' : ''}>
${this.documentationSearches?.map(search =>
this.renderDocumentationList(search)
)}
@@ -83,7 +83,7 @@
return html`
<tr class="table">
<td class="name">
- <a href="${this.computeSearchUrl(search.url)}">${search.title}</a>
+ <a href=${this.computeSearchUrl(search.url)}>${search.title}</a>
</td>
<td></td>
<td></td>
diff --git a/polygerrit-ui/app/elements/edit/gr-default-editor/gr-default-editor.ts b/polygerrit-ui/app/elements/edit/gr-default-editor/gr-default-editor.ts
index 5312be2..8b8a615 100644
--- a/polygerrit-ui/app/elements/edit/gr-default-editor/gr-default-editor.ts
+++ b/polygerrit-ui/app/elements/edit/gr-default-editor/gr-default-editor.ts
@@ -61,7 +61,7 @@
override render() {
return html` <textarea
id="textarea"
- .value="${this.fileContent}"
+ .value=${this.fileContent}
@input=${this._handleTextareaInput}
></textarea>`;
}
diff --git a/polygerrit-ui/app/elements/edit/gr-edit-controls/gr-edit-controls.ts b/polygerrit-ui/app/elements/edit/gr-edit-controls/gr-edit-controls.ts
index ae81f07..f081037 100644
--- a/polygerrit-ui/app/elements/edit/gr-edit-controls/gr-edit-controls.ts
+++ b/polygerrit-ui/app/elements/edit/gr-edit-controls/gr-edit-controls.ts
@@ -155,8 +155,8 @@
private renderAction(action: GrEditAction) {
return html`
<gr-button
- id="${action.id}"
- class="${this.computeIsInvisible(action.id)}"
+ id=${action.id}
+ class=${this.computeIsInvisible(action.id)}
link=""
@click=${this.handleTap}
>${action.label}</gr-button
diff --git a/polygerrit-ui/app/elements/edit/gr-edit-file-controls/gr-edit-file-controls.ts b/polygerrit-ui/app/elements/edit/gr-edit-file-controls/gr-edit-file-controls.ts
index 1b854b4..a770d5b 100644
--- a/polygerrit-ui/app/elements/edit/gr-edit-file-controls/gr-edit-file-controls.ts
+++ b/polygerrit-ui/app/elements/edit/gr-edit-file-controls/gr-edit-file-controls.ts
@@ -82,7 +82,7 @@
.items=${fileActions}
down-arrow=""
vertical-offset="20"
- @tap-item="${this._handleActionTap}"
+ @tap-item=${this._handleActionTap}
link=""
>Actions</gr-dropdown
>`;
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 40515d9..dc8e7a6 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
@@ -218,7 +218,7 @@
<span>Edit mode</span>
<span class="separator"></span>
<gr-editable-label
- label-text="File path"
+ labelText="File path"
.value=${this.path}
placeholder="File path..."
@changed=${this.handlePathChanged}
diff --git a/polygerrit-ui/app/elements/plugins/gr-dom-hooks/gr-dom-hooks.ts b/polygerrit-ui/app/elements/plugins/gr-dom-hooks/gr-dom-hooks.ts
index 6b8c4f0..1ca9918 100644
--- a/polygerrit-ui/app/elements/plugins/gr-dom-hooks/gr-dom-hooks.ts
+++ b/polygerrit-ui/app/elements/plugins/gr-dom-hooks/gr-dom-hooks.ts
@@ -1,20 +1,8 @@
/**
* @license
- * Copyright (C) 2017 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
+ * Copyright 2017 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
*/
-import {PolymerElement} from '@polymer/polymer/polymer-element';
import {PluginApi} from '../../../api/plugin';
import {HookApi, HookCallback, PluginElement} from '../../../api/hook';
@@ -81,20 +69,17 @@
}
_createPlaceholder(hookName: string) {
- class HookPlaceholder extends PolymerElement {
- static get is() {
- return hookName;
- }
+ /**
+ * See gr-endpoint-decorator.ts for how hooks are instantiated and
+ * initialized.
+ */
+ class HookPlaceholder extends HTMLElement {
+ plugin?: PluginApi;
- static get properties() {
- return {
- plugin: Object,
- content: Object,
- };
- }
+ content?: Element | null;
}
- customElements.define(HookPlaceholder.is, HookPlaceholder);
+ customElements.define(hookName, HookPlaceholder);
}
handleInstanceDetached(instance: T) {
diff --git a/polygerrit-ui/app/elements/plugins/gr-dom-hooks/gr-dom-hooks_test.ts b/polygerrit-ui/app/elements/plugins/gr-dom-hooks/gr-dom-hooks_test.ts
index fde699d..6001622 100644
--- a/polygerrit-ui/app/elements/plugins/gr-dom-hooks/gr-dom-hooks_test.ts
+++ b/polygerrit-ui/app/elements/plugins/gr-dom-hooks/gr-dom-hooks_test.ts
@@ -1,20 +1,8 @@
/**
* @license
- * Copyright (C) 2017 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
+ * Copyright 2017 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
*/
-
import {HookApi, PluginElement} from '../../../api/hook';
import {PluginApi} from '../../../api/plugin';
import '../../../test/common-test-setup-karma';
diff --git a/polygerrit-ui/app/elements/settings/gr-agreements-list/gr-agreements-list.ts b/polygerrit-ui/app/elements/settings/gr-agreements-list/gr-agreements-list.ts
index 2db7c76..96b8d12 100644
--- a/polygerrit-ui/app/elements/settings/gr-agreements-list/gr-agreements-list.ts
+++ b/polygerrit-ui/app/elements/settings/gr-agreements-list/gr-agreements-list.ts
@@ -62,7 +62,7 @@
return html`
<tr>
<td class="nameColumn">
- <a href="${this.getUrlBase(agreement?.url)}" rel="external">
+ <a href=${this.getUrlBase(agreement?.url)} rel="external">
${agreement.name}
</a>
</td>
@@ -86,7 +86,7 @@
)}
</tbody>
</table>
- <a href="${this.getUrl()}">New Contributor Agreement</a>
+ <a href=${this.getUrl()}>New Contributor Agreement</a>
</div>`;
}
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 eff22d1..34e376b 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
@@ -15,27 +15,24 @@
* limitations under the License.
*/
import '../../shared/gr-button/gr-button';
-import '../../../styles/shared-styles';
-import '../../../styles/gr-form-styles';
-import {dom, EventApi} from '@polymer/polymer/lib/legacy/polymer.dom';
-import {PolymerElement} from '@polymer/polymer/polymer-element';
-import {htmlTemplate} from './gr-change-table-editor_html';
-import {customElement, property, observe} from '@polymer/decorators';
import {ServerInfo} from '../../../types/common';
import {getAppContext} from '../../../services/app-context';
import {columnNames} from '../../change-list/gr-change-list/gr-change-list';
import {KnownExperimentId} from '../../../services/flags/flags';
+import {LitElement, css, html} from 'lit';
+import {customElement, property} from 'lit/decorators';
+import {sharedStyles} from '../../../styles/shared-styles';
+import {formStyles} from '../../../styles/gr-form-styles';
+import {PropertyValues} from 'lit';
+import {fire} from '../../../utils/event-util';
+import {ValueChangedEvent} from '../../../types/events';
@customElement('gr-change-table-editor')
-export class GrChangeTableEditor extends PolymerElement {
- static get template() {
- return htmlTemplate;
- }
-
- @property({type: Array, notify: true})
+export class GrChangeTableEditor extends LitElement {
+ @property({type: Array})
displayedColumns: string[] = [];
- @property({type: Boolean, notify: true})
+ @property({type: Boolean})
showNumber?: boolean;
@property({type: Object})
@@ -46,22 +43,97 @@
private readonly flagsService = getAppContext().flagsService;
- @observe('serverConfig')
- _configChanged(config: ServerInfo) {
- this.defaultColumns = columnNames.filter(col =>
- this._isColumnEnabled(col, config)
+ static override styles = [
+ sharedStyles,
+ formStyles,
+ css`
+ #changeCols {
+ width: auto;
+ }
+ #changeCols .visibleHeader {
+ text-align: center;
+ }
+ .checkboxContainer {
+ cursor: pointer;
+ text-align: center;
+ }
+ .checkboxContainer input {
+ cursor: pointer;
+ }
+ .checkboxContainer:hover {
+ outline: 1px solid var(--border-color);
+ }
+ `,
+ ];
+
+ override render() {
+ return html`<div class="gr-form-styles">
+ <table id="changeCols">
+ <thead>
+ <tr>
+ <th class="nameHeader">Column</th>
+ <th class="visibleHeader">Visible</th>
+ </tr>
+ </thead>
+ <tbody>
+ <tr>
+ <td><label for="numberCheckbox">Number</label></td>
+ <td
+ class="checkboxContainer"
+ @click=${this.handleCheckboxContainerClick}
+ >
+ <input
+ id="numberCheckbox"
+ type="checkbox"
+ name="number"
+ @click=${this.handleNumberCheckboxClick}
+ ?checked=${this.showNumber}
+ />
+ </td>
+ </tr>
+ ${this.defaultColumns.map(column => this.renderRow(column))}
+ </tbody>
+ </table>
+ </div>`;
+ }
+
+ renderRow(column: string) {
+ return html`<tr>
+ <td><label for=${column}>${column}</label></td>
+ <td class="checkboxContainer" @click=${this.handleCheckboxContainerClick}>
+ <input
+ id=${column}
+ type="checkbox"
+ name=${column}
+ @click=${this.handleTargetClick}
+ ?checked=${!this.computeIsColumnHidden(column)}
+ />
+ </td>
+ </tr>`;
+ }
+
+ override willUpdate(changedProperties: PropertyValues) {
+ if (changedProperties.has('serverConfig')) {
+ this.configChanged();
+ }
+ }
+
+ private configChanged() {
+ this.defaultColumns = columnNames.filter(column =>
+ this.isColumnEnabled(column)
);
if (!this.displayedColumns) return;
this.displayedColumns = this.displayedColumns.filter(column =>
- this._isColumnEnabled(column, config)
+ this.isColumnEnabled(column)
);
}
/**
* Is the column disabled by a server config or experiment?
+ * private but used in test
*/
- _isColumnEnabled(column: string, config: ServerInfo) {
- if (!config || !config.change) return true;
+ isColumnEnabled(column: string) {
+ if (!this.serverConfig?.change) return true;
if (column === 'Comments')
return this.flagsService.isEnabled('comments-column');
if (column === 'Status')
@@ -78,11 +150,12 @@
/**
* Get the list of enabled column names from whichever checkboxes are
* checked (excluding the number checkbox).
+ * private but used in test
*/
- _getDisplayedColumns() {
- if (this.root === null) return [];
+ getDisplayedColumns() {
+ if (this.shadowRoot === null) return [];
return Array.from(
- this.root.querySelectorAll<HTMLInputElement>(
+ this.shadowRoot.querySelectorAll<HTMLInputElement>(
'.checkboxContainer input:not([name=number])'
)
)
@@ -90,18 +163,18 @@
.map(checkbox => checkbox.name);
}
- _computeIsColumnHidden(columnToCheck?: string, columnsToDisplay?: string[]) {
- if (!columnsToDisplay || !columnToCheck) {
+ private computeIsColumnHidden(columnToCheck?: string) {
+ if (!this.displayedColumns || !columnToCheck) {
return false;
}
- return !columnsToDisplay.includes(columnToCheck);
+ return !this.displayedColumns.includes(columnToCheck);
}
/**
* Handle a click on a checkbox container and relay the click to the checkbox it
* contains.
*/
- _handleCheckboxContainerClick(e: MouseEvent) {
+ private handleCheckboxContainerClick(e: MouseEvent) {
if (e.target === null) return;
const checkbox = (e.target as HTMLElement).querySelector('input');
if (!checkbox) {
@@ -114,22 +187,26 @@
* Handle a click on the number checkbox and update the showNumber property
* accordingly.
*/
- _handleNumberCheckboxClick(e: MouseEvent) {
- this.showNumber = (
- (dom(e) as EventApi).rootTarget as HTMLInputElement
- ).checked;
+ private handleNumberCheckboxClick(e: MouseEvent) {
+ this.showNumber = (e.target as HTMLInputElement).checked;
+ fire(this, 'show-number-changed', {value: this.showNumber});
}
/**
* Handle a click on a displayed column checkboxes (excluding number) and
* update the displayedColumns property accordingly.
*/
- _handleTargetClick() {
- this.set('displayedColumns', this._getDisplayedColumns());
+ private handleTargetClick() {
+ this.displayedColumns = this.getDisplayedColumns();
+ fire(this, 'displayed-columns-changed', {value: this.displayedColumns});
}
}
declare global {
+ interface HTMLElementEventMap {
+ 'show-number-changed': ValueChangedEvent<boolean>;
+ 'displayed-columns-changed': ValueChangedEvent<string[]>;
+ }
interface HTMLElementTagNameMap {
'gr-change-table-editor': GrChangeTableEditor;
}
diff --git a/polygerrit-ui/app/elements/settings/gr-change-table-editor/gr-change-table-editor_html.ts b/polygerrit-ui/app/elements/settings/gr-change-table-editor/gr-change-table-editor_html.ts
deleted file mode 100644
index e756a20..0000000
--- a/polygerrit-ui/app/elements/settings/gr-change-table-editor/gr-change-table-editor_html.ts
+++ /dev/null
@@ -1,85 +0,0 @@
-/**
- * @license
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-import {html} from '@polymer/polymer/lib/utils/html-tag';
-
-export const htmlTemplate = html`
- <style include="shared-styles">
- /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
- </style>
- <style include="gr-form-styles">
- #changeCols {
- width: auto;
- }
- #changeCols .visibleHeader {
- text-align: center;
- }
- .checkboxContainer {
- cursor: pointer;
- text-align: center;
- }
- .checkboxContainer input {
- cursor: pointer;
- }
- .checkboxContainer:hover {
- outline: 1px solid var(--border-color);
- }
- </style>
- <div class="gr-form-styles">
- <table id="changeCols">
- <thead>
- <tr>
- <th class="nameHeader">Column</th>
- <th class="visibleHeader">Visible</th>
- </tr>
- </thead>
- <tbody>
- <tr>
- <td><label for="numberCheckbox">Number</label></td>
- <td
- class="checkboxContainer"
- on-click="_handleCheckboxContainerClick"
- >
- <input
- id="numberCheckbox"
- type="checkbox"
- name="number"
- on-click="_handleNumberCheckboxClick"
- checked$="[[showNumber]]"
- />
- </td>
- </tr>
- <template is="dom-repeat" items="[[defaultColumns]]">
- <tr>
- <td><label for$="[[item]]">[[item]]</label></td>
- <td
- class="checkboxContainer"
- on-click="_handleCheckboxContainerClick"
- >
- <input
- id$="[[item]]"
- type="checkbox"
- name="[[item]]"
- on-click="_handleTargetClick"
- checked$="[[!_computeIsColumnHidden(item, displayedColumns)]]"
- />
- </td>
- </tr>
- </template>
- </tbody>
- </table>
- </div>
-`;
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 c2bcec2..c37c3f9 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
@@ -14,21 +14,21 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-import * as MockInteractions from '@polymer/iron-test-helpers/mock-interactions';
import '../../../test/common-test-setup-karma';
import './gr-change-table-editor';
import {GrChangeTableEditor} from './gr-change-table-editor';
import {queryAndAssert} from '../../../test/test-utils';
import {createServerInfo} from '../../../test/test-data-generators';
-
-const basicFixture = fixtureFromElement('gr-change-table-editor');
+import {fixture, html} from '@open-wc/testing-helpers';
suite('gr-change-table-editor tests', () => {
let element: GrChangeTableEditor;
let columns: string[];
setup(async () => {
- element = basicFixture.instantiate();
+ element = await fixture<GrChangeTableEditor>(
+ html`<gr-change-table-editor></gr-change-table-editor>`
+ );
columns = [
'Subject',
@@ -41,10 +41,84 @@
'Updated',
];
- element.set('displayedColumns', columns);
+ element.displayedColumns = columns;
element.showNumber = false;
element.serverConfig = createServerInfo();
- await flush();
+ await element.updateComplete;
+ });
+
+ test('renders', () => {
+ expect(element).shadowDom.to.equal(/* HTML */ ` <div class="gr-form-styles">
+ <table id="changeCols">
+ <thead>
+ <tr>
+ <th class="nameHeader">Column</th>
+ <th class="visibleHeader">Visible</th>
+ </tr>
+ </thead>
+ <tbody>
+ <tr>
+ <td><label for="numberCheckbox"> Number </label></td>
+ <td class="checkboxContainer">
+ <input id="numberCheckbox" name="number" type="checkbox" />
+ </td>
+ </tr>
+ <tr>
+ <td><label for="Subject"> Subject </label></td>
+ <td class="checkboxContainer">
+ <input checked="" id="Subject" name="Subject" type="checkbox" />
+ </td>
+ </tr>
+ <tr>
+ <td><label for="Status"> Status </label></td>
+ <td class="checkboxContainer">
+ <input checked="" id="Status" name="Status" type="checkbox" />
+ </td>
+ </tr>
+ <tr>
+ <td><label for="Owner"> Owner </label></td>
+ <td class="checkboxContainer">
+ <input checked="" id="Owner" name="Owner" type="checkbox" />
+ </td>
+ </tr>
+ <tr>
+ <td><label for="Reviewers"> Reviewers </label></td>
+ <td class="checkboxContainer">
+ <input
+ checked=""
+ id="Reviewers"
+ name="Reviewers"
+ type="checkbox"
+ />
+ </td>
+ </tr>
+ <tr>
+ <td><label for="Repo"> Repo </label></td>
+ <td class="checkboxContainer">
+ <input checked="" id="Repo" name="Repo" type="checkbox" />
+ </td>
+ </tr>
+ <tr>
+ <td><label for="Branch"> Branch </label></td>
+ <td class="checkboxContainer">
+ <input checked="" id="Branch" name="Branch" type="checkbox" />
+ </td>
+ </tr>
+ <tr>
+ <td><label for="Updated"> Updated </label></td>
+ <td class="checkboxContainer">
+ <input checked="" id="Updated" name="Updated" type="checkbox" />
+ </td>
+ </tr>
+ <tr>
+ <td><label for="Size"> Size </label></td>
+ <td class="checkboxContainer">
+ <input id="Size" name="Size" type="checkbox" />
+ </td>
+ </tr>
+ </tbody>
+ </table>
+ </div>`);
});
test('renders', () => {
@@ -60,7 +134,7 @@
}
});
- test('hide item', () => {
+ test('hide item', async () => {
const checkbox = queryAndAssert<HTMLInputElement>(
element,
'table tr:nth-child(2) input'
@@ -69,23 +143,17 @@
const displayedLength = element.displayedColumns.length;
assert.isTrue(isChecked);
- MockInteractions.tap(checkbox);
- flush();
+ checkbox.click();
+ await element.updateComplete;
assert.equal(element.displayedColumns.length, displayedLength - 1);
});
- test('show item', () => {
- element.set('displayedColumns', [
- 'Status',
- 'Owner',
- 'Repo',
- 'Branch',
- 'Updated',
- ]);
+ test('show item', async () => {
+ element.displayedColumns = ['Status', 'Owner', 'Repo', 'Branch', 'Updated'];
// trigger computation of enabled displayed columns
element.serverConfig = createServerInfo();
- flush();
+ await element.updateComplete;
const checkbox = queryAndAssert<HTMLInputElement>(
element,
'table tr:nth-child(2) input'
@@ -96,74 +164,68 @@
const table = queryAndAssert<HTMLTableElement>(element, 'table');
assert.equal(table.style.display, '');
- MockInteractions.tap(checkbox);
- flush();
+ checkbox.click();
+ await element.updateComplete;
assert.equal(element.displayedColumns.length, displayedLength + 1);
});
- test('_getDisplayedColumns', () => {
+ test('getDisplayedColumns', () => {
const enabledColumns = columns.filter(column =>
- element._isColumnEnabled(column, element.serverConfig!)
+ element.isColumnEnabled(column)
);
- assert.deepEqual(element._getDisplayedColumns(), enabledColumns);
+ assert.deepEqual(element.getDisplayedColumns(), enabledColumns);
const input = queryAndAssert<HTMLInputElement>(
element,
'.checkboxContainer input[name=Subject]'
);
- MockInteractions.tap(input);
+ input.click();
assert.deepEqual(
- element._getDisplayedColumns(),
+ element.getDisplayedColumns(),
enabledColumns.filter(c => c !== 'Subject')
);
});
- test('_handleCheckboxContainerClick relays taps to checkboxes', () => {
- const checkBoxClickStub = sinon.stub(element, '_handleNumberCheckboxClick');
- const targetClickStub = sinon.stub(element, '_handleTargetClick');
-
- const firstContainer = queryAndAssert(
+ test('handleCheckboxContainerClick relays taps to checkboxes', async () => {
+ const firstContainer = queryAndAssert<HTMLTableRowElement>(
element,
'table tr:first-of-type .checkboxContainer'
);
- MockInteractions.tap(firstContainer);
- assert.isTrue(checkBoxClickStub.calledOnce);
- assert.isFalse(targetClickStub.called);
+ assert.isFalse(element.showNumber);
+ firstContainer.click();
+ assert.isTrue(element.showNumber);
- const lastContainer = queryAndAssert(
+ const lastContainer = queryAndAssert<HTMLTableRowElement>(
element,
'table tr:last-of-type .checkboxContainer'
);
- MockInteractions.tap(lastContainer);
- assert.isTrue(checkBoxClickStub.calledOnce);
- assert.isTrue(targetClickStub.calledOnce);
+ const lastColumn =
+ element.defaultColumns[element.defaultColumns.length - 1];
+ assert.notInclude(element.displayedColumns, lastColumn);
+ lastContainer.click();
+ await element.updateComplete;
+ assert.include(element.displayedColumns, lastColumn);
});
- test('_handleNumberCheckboxClick', () => {
- const checkBoxClickSpy = sinon.spy(element, '_handleNumberCheckboxClick');
-
- const numberInput = queryAndAssert(
+ test('handleNumberCheckboxClick', () => {
+ const numberInput = queryAndAssert<HTMLInputElement>(
element,
'.checkboxContainer input[name=number]'
);
- MockInteractions.tap(numberInput);
- assert.isTrue(checkBoxClickSpy.calledOnce);
+ numberInput.click();
assert.isTrue(element.showNumber);
- MockInteractions.tap(numberInput);
- assert.isTrue(checkBoxClickSpy.calledTwice);
+ numberInput.click();
assert.isFalse(element.showNumber);
});
- test('_handleTargetClick', () => {
- const targetClickSpy = sinon.spy(element, '_handleTargetClick');
+ test('handleTargetClick', () => {
assert.include(element.displayedColumns, 'Subject');
- const subjectInput = queryAndAssert(
+ const subjectInput = queryAndAssert<HTMLInputElement>(
element,
'.checkboxContainer input[name=Subject]'
);
- MockInteractions.tap(subjectInput);
- assert.isTrue(targetClickSpy.calledOnce);
+ subjectInput.click();
assert.notInclude(element.displayedColumns, 'Subject');
});
});
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 db8a008..f429c22 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
@@ -132,8 +132,8 @@
id="claNewAgreementsInput${item.name}"
name="claNewAgreementsRadio"
type="radio"
- data-name="${ifDefined(item.name)}"
- data-url="${ifDefined(item.url)}"
+ data-name=${ifDefined(item.name)}
+ data-url=${ifDefined(item.url)}
@click=${this.handleShowAgreement}
?disabled=${this.disableAgreements(item)}
/>
@@ -159,7 +159,7 @@
<h3 class="heading-3">Review the agreement:</h3>
<div id="agreementsUrl" class="agreementsUrl">
<a
- href="${ifDefined(this.agreementsUrl)}"
+ href=${ifDefined(this.agreementsUrl)}
target="blank"
rel="noopener"
>
diff --git a/polygerrit-ui/app/elements/settings/gr-edit-preferences/gr-edit-preferences.ts b/polygerrit-ui/app/elements/settings/gr-edit-preferences/gr-edit-preferences.ts
index c18ea78..0f7e065 100644
--- a/polygerrit-ui/app/elements/settings/gr-edit-preferences/gr-edit-preferences.ts
+++ b/polygerrit-ui/app/elements/settings/gr-edit-preferences/gr-edit-preferences.ts
@@ -92,7 +92,7 @@
return html`
<h2
id="EditPreferences"
- class="${this.hasUnsavedChanges() ? 'edited' : ''}"
+ class=${this.hasUnsavedChanges() ? 'edited' : ''}
>
Edit Preferences
</h2>
diff --git a/polygerrit-ui/app/elements/settings/gr-email-editor/gr-email-editor.ts b/polygerrit-ui/app/elements/settings/gr-email-editor/gr-email-editor.ts
index afd04b4..40c0690 100644
--- a/polygerrit-ui/app/elements/settings/gr-email-editor/gr-email-editor.ts
+++ b/polygerrit-ui/app/elements/settings/gr-email-editor/gr-email-editor.ts
@@ -16,73 +16,147 @@
*/
import '@polymer/iron-input/iron-input';
import '../../shared/gr-button/gr-button';
-import '../../../styles/shared-styles';
-import {dom, EventApi} from '@polymer/polymer/lib/legacy/polymer.dom';
-import {PolymerElement} from '@polymer/polymer/polymer-element';
-import {htmlTemplate} from './gr-email-editor_html';
-import {customElement, property} from '@polymer/decorators';
import {EmailInfo} from '../../../types/common';
import {getAppContext} from '../../../services/app-context';
+import {LitElement, css, html} from 'lit';
+import {customElement, property, state} from 'lit/decorators';
+import {sharedStyles} from '../../../styles/shared-styles';
+import {formStyles} from '../../../styles/gr-form-styles';
+import {ValueChangedEvent} from '../../../types/events';
+import {fire} from '../../../utils/event-util';
@customElement('gr-email-editor')
-export class GrEmailEditor extends PolymerElement {
- static get template() {
- return htmlTemplate;
- }
+export class GrEmailEditor extends LitElement {
+ @property({type: Boolean}) hasUnsavedChanges = false;
- @property({type: Boolean, notify: true})
- hasUnsavedChanges = false;
+ /* private but used in test */
+ @state() emails: EmailInfo[] = [];
- @property({type: Array})
- _emails: EmailInfo[] = [];
+ /* private but used in test */
+ @state() emailsToRemove: EmailInfo[] = [];
- @property({type: Array})
- _emailsToRemove: EmailInfo[] = [];
-
- @property({type: String})
- _newPreferred: string | null = null;
+ /* private but used in test */
+ @state() newPreferred = '';
readonly restApiService = getAppContext().restApiService;
+ static override styles = [
+ sharedStyles,
+ formStyles,
+ css`
+ th {
+ color: var(--deemphasized-text-color);
+ text-align: left;
+ }
+ #emailTable .emailColumn {
+ min-width: 32.5em;
+ width: auto;
+ }
+ #emailTable .preferredHeader {
+ text-align: center;
+ width: 6em;
+ }
+ #emailTable .preferredControl {
+ cursor: pointer;
+ height: auto;
+ text-align: center;
+ }
+ #emailTable .preferredControl .preferredRadio {
+ height: auto;
+ }
+ .preferredControl:hover {
+ outline: 1px solid var(--border-color);
+ }
+ `,
+ ];
+
+ override render() {
+ return html`<div class="gr-form-styles">
+ <table id="emailTable">
+ <thead>
+ <tr>
+ <th class="emailColumn">Email</th>
+ <th class="preferredHeader">Preferred</th>
+ <th></th>
+ </tr>
+ </thead>
+ <tbody>
+ ${this.emails.map((email, index) => this.renderEmail(email, index))}
+ </tbody>
+ </table>
+ </div>`;
+ }
+
+ private renderEmail(email: EmailInfo, index: number) {
+ return html`<tr>
+ <td class="emailColumn">${email.email}</td>
+ <td class="preferredControl" @click=${this.handlePreferredControlClick}>
+ <iron-input
+ class="preferredRadio"
+ @change=${this.handlePreferredChange}
+ .bindValue=${email.email}
+ >
+ <input
+ class="preferredRadio"
+ type="radio"
+ @change=${this.handlePreferredChange}
+ name="preferred"
+ ?checked=${email.preferred}
+ />
+ </iron-input>
+ </td>
+ <td>
+ <gr-button
+ data-index=${index}
+ @click=${this.handleDeleteButton}
+ ?disabled=${this.checkPreferred(email.preferred)}
+ class="remove-button"
+ >Delete</gr-button
+ >
+ </td>
+ </tr>`;
+ }
+
loadData() {
return this.restApiService.getAccountEmails().then(emails => {
- this._emails = emails ?? [];
+ this.emails = emails ?? [];
});
}
save() {
const promises: Promise<unknown>[] = [];
- for (const emailObj of this._emailsToRemove) {
+ for (const emailObj of this.emailsToRemove) {
promises.push(this.restApiService.deleteAccountEmail(emailObj.email));
}
- if (this._newPreferred) {
+ if (this.newPreferred) {
promises.push(
- this.restApiService.setPreferredAccountEmail(this._newPreferred)
+ this.restApiService.setPreferredAccountEmail(this.newPreferred)
);
}
return Promise.all(promises).then(() => {
- this._emailsToRemove = [];
- this._newPreferred = null;
- this.hasUnsavedChanges = false;
+ this.emailsToRemove = [];
+ this.newPreferred = '';
+ this.setHasUnsavedChanges(false);
});
}
- _handleDeleteButton(e: Event) {
- const target = (dom(e) as EventApi).localTarget;
+ private handleDeleteButton(e: Event) {
+ const target = e.target;
if (!(target instanceof Element)) return;
const indexStr = target.getAttribute('data-index');
if (indexStr === null) return;
const index = Number(indexStr);
- const email = this._emails[index];
- this.push('_emailsToRemove', email);
- this.splice('_emails', index, 1);
- this.hasUnsavedChanges = true;
+ const email = this.emails[index];
+ this.emailsToRemove = [...this.emailsToRemove, email];
+ this.emails.splice(index, 1);
+ this.requestUpdate();
+ this.setHasUnsavedChanges(true);
}
- _handlePreferredControlClick(e: Event) {
+ private handlePreferredControlClick(e: Event) {
if (
e.target instanceof HTMLElement &&
e.target.classList.contains('preferredControl') &&
@@ -92,26 +166,36 @@
}
}
- _handlePreferredChange(e: Event) {
+ private handlePreferredChange(e: Event) {
if (!(e.target instanceof HTMLInputElement)) return;
const preferred = e.target.value;
- for (let i = 0; i < this._emails.length; i++) {
- if (preferred === this._emails[i].email) {
- this.set(['_emails', i, 'preferred'], true);
- this._newPreferred = preferred;
- this.hasUnsavedChanges = true;
- } else if (this._emails[i].preferred) {
- this.set(['_emails', i, 'preferred'], false);
+ for (let i = 0; i < this.emails.length; i++) {
+ if (preferred === this.emails[i].email) {
+ this.emails[i].preferred = true;
+ this.requestUpdate();
+ this.newPreferred = preferred;
+ this.setHasUnsavedChanges(true);
+ } else if (this.emails[i].preferred) {
+ this.emails[i].preferred = false;
+ this.requestUpdate();
}
}
}
- _checkPreferred(preferred?: boolean) {
+ private checkPreferred(preferred?: boolean) {
return preferred ?? false;
}
+
+ private setHasUnsavedChanges(value: boolean) {
+ this.hasUnsavedChanges = value;
+ fire(this, 'has-unsaved-changes-changed', {value});
+ }
}
declare global {
+ interface HTMLElementEventMap {
+ 'has-unsaved-changes-changed': ValueChangedEvent<boolean>;
+ }
interface HTMLElementTagNameMap {
'gr-email-editor': GrEmailEditor;
}
diff --git a/polygerrit-ui/app/elements/settings/gr-email-editor/gr-email-editor_html.ts b/polygerrit-ui/app/elements/settings/gr-email-editor/gr-email-editor_html.ts
deleted file mode 100644
index 666afb7..0000000
--- a/polygerrit-ui/app/elements/settings/gr-email-editor/gr-email-editor_html.ts
+++ /dev/null
@@ -1,96 +0,0 @@
-/**
- * @license
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-import {html} from '@polymer/polymer/lib/utils/html-tag';
-
-export const htmlTemplate = html`
- <style include="shared-styles">
- /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
- </style>
- <style include="gr-form-styles">
- th {
- color: var(--deemphasized-text-color);
- text-align: left;
- }
- #emailTable .emailColumn {
- min-width: 32.5em;
- width: auto;
- }
- #emailTable .preferredHeader {
- text-align: center;
- width: 6em;
- }
- #emailTable .preferredControl {
- cursor: pointer;
- height: auto;
- text-align: center;
- }
- #emailTable .preferredControl .preferredRadio {
- height: auto;
- }
- .preferredControl:hover {
- outline: 1px solid var(--border-color);
- }
- </style>
- <div class="gr-form-styles">
- <table id="emailTable">
- <thead>
- <tr>
- <th class="emailColumn">Email</th>
- <th class="preferredHeader">Preferred</th>
- <th></th>
- </tr>
- </thead>
- <tbody>
- <template is="dom-repeat" items="[[_emails]]">
- <tr>
- <td class="emailColumn">[[item.email]]</td>
- <td
- class="preferredControl"
- on-click="_handlePreferredControlClick"
- >
- <iron-input
- class="preferredRadio"
- type="radio"
- on-change="_handlePreferredChange"
- name="preferred"
- bind-value="[[item.email]]"
- checked$="[[item.preferred]]"
- >
- <input
- class="preferredRadio"
- type="radio"
- on-change="_handlePreferredChange"
- name="preferred"
- checked$="[[item.preferred]]"
- />
- </iron-input>
- </td>
- <td>
- <gr-button
- data-index$="[[index]]"
- on-click="_handleDeleteButton"
- disabled="[[_checkPreferred(item.preferred)]]"
- class="remove-button"
- >Delete</gr-button
- >
- </td>
- </tr>
- </template>
- </tbody>
- </table>
- </div>
-`;
diff --git a/polygerrit-ui/app/elements/settings/gr-email-editor/gr-email-editor_test.ts b/polygerrit-ui/app/elements/settings/gr-email-editor/gr-email-editor_test.ts
index e478381..8ac101d 100644
--- a/polygerrit-ui/app/elements/settings/gr-email-editor/gr-email-editor_test.ts
+++ b/polygerrit-ui/app/elements/settings/gr-email-editor/gr-email-editor_test.ts
@@ -19,8 +19,7 @@
import './gr-email-editor';
import {GrEmailEditor} from './gr-email-editor';
import {spyRestApi, stubRestApi} from '../../../test/test-utils';
-
-const basicFixture = fixtureFromElement('gr-email-editor');
+import {fixture, html} from '@open-wc/testing-helpers';
suite('gr-email-editor tests', () => {
let element: GrEmailEditor;
@@ -34,10 +33,102 @@
stubRestApi('getAccountEmails').returns(Promise.resolve(emails));
- element = basicFixture.instantiate();
+ element = await fixture<GrEmailEditor>(
+ html`<gr-email-editor></gr-email-editor>`
+ );
await element.loadData();
- await flush();
+ await element.updateComplete;
+ });
+
+ test('renders', () => {
+ expect(element).shadowDom.to.equal(/* HTML */ `<div class="gr-form-styles">
+ <table id="emailTable">
+ <thead>
+ <tr>
+ <th class="emailColumn">Email</th>
+ <th class="preferredHeader">Preferred</th>
+ <th></th>
+ </tr>
+ </thead>
+ <tbody>
+ <tr>
+ <td class="emailColumn">email@one.com</td>
+ <td class="preferredControl">
+ <iron-input class="preferredRadio">
+ <input
+ class="preferredRadio"
+ name="preferred"
+ type="radio"
+ value="email@one.com"
+ />
+ </iron-input>
+ </td>
+ <td>
+ <gr-button
+ aria-disabled="false"
+ class="remove-button"
+ data-index="0"
+ role="button"
+ tabindex="0"
+ >
+ Delete
+ </gr-button>
+ </td>
+ </tr>
+ <tr>
+ <td class="emailColumn">email@two.com</td>
+ <td class="preferredControl">
+ <iron-input class="preferredRadio">
+ <input
+ checked=""
+ class="preferredRadio"
+ name="preferred"
+ type="radio"
+ value="email@two.com"
+ />
+ </iron-input>
+ </td>
+ <td>
+ <gr-button
+ aria-disabled="true"
+ class="remove-button"
+ data-index="1"
+ disabled=""
+ role="button"
+ tabindex="-1"
+ >
+ Delete
+ </gr-button>
+ </td>
+ </tr>
+ <tr>
+ <td class="emailColumn">email@three.com</td>
+ <td class="preferredControl">
+ <iron-input class="preferredRadio">
+ <input
+ class="preferredRadio"
+ name="preferred"
+ type="radio"
+ value="email@three.com"
+ />
+ </iron-input>
+ </td>
+ <td>
+ <gr-button
+ aria-disabled="false"
+ class="remove-button"
+ data-index="2"
+ role="button"
+ tabindex="0"
+ >
+ Delete
+ </gr-button>
+ </td>
+ </tr>
+ </tbody>
+ </table>
+ </div>`);
});
test('renders', () => {
@@ -66,28 +157,27 @@
});
test('edit preferred', () => {
- const preferredChangedSpy = sinon.spy(element, '_handlePreferredChange');
const radios = element
.shadowRoot!.querySelector('table')!
.querySelectorAll<HTMLInputElement>('input[type=radio]');
assert.isFalse(element.hasUnsavedChanges);
- assert.isNotOk(element._newPreferred);
- assert.equal(element._emailsToRemove.length, 0);
- assert.equal(element._emails.length, 3);
+ assert.isNotOk(element.newPreferred);
+ assert.equal(element.emailsToRemove.length, 0);
+ assert.equal(element.emails.length, 3);
assert.isNotOk(radios[0].checked);
assert.isOk(radios[1].checked);
- assert.isFalse(preferredChangedSpy.called);
+ assert.isUndefined(element.emails[0].preferred);
radios[0].click();
assert.isTrue(element.hasUnsavedChanges);
- assert.isOk(element._newPreferred);
- assert.equal(element._emailsToRemove.length, 0);
- assert.equal(element._emails.length, 3);
+ assert.isOk(element.newPreferred);
+ assert.equal(element.emailsToRemove.length, 0);
+ assert.equal(element.emails.length, 3);
assert.isOk(radios[0].checked);
assert.isNotOk(radios[1].checked);
- assert.isTrue(preferredChangedSpy.called);
+ assert.isTrue(element.emails[0].preferred);
});
test('delete email', () => {
@@ -96,18 +186,18 @@
.querySelectorAll('gr-button');
assert.isFalse(element.hasUnsavedChanges);
- assert.isNotOk(element._newPreferred);
- assert.equal(element._emailsToRemove.length, 0);
- assert.equal(element._emails.length, 3);
+ assert.isNotOk(element.newPreferred);
+ assert.equal(element.emailsToRemove.length, 0);
+ assert.equal(element.emails.length, 3);
buttons[2].click();
assert.isTrue(element.hasUnsavedChanges);
- assert.isNotOk(element._newPreferred);
- assert.equal(element._emailsToRemove.length, 1);
- assert.equal(element._emails.length, 2);
+ assert.isNotOk(element.newPreferred);
+ assert.equal(element.emailsToRemove.length, 1);
+ assert.equal(element.emails.length, 2);
- assert.equal(element._emailsToRemove[0].email, 'email@three.com');
+ assert.equal(element.emailsToRemove[0].email, 'email@three.com');
});
test('save changes', async () => {
@@ -119,19 +209,19 @@
.querySelectorAll('tbody tr');
assert.isFalse(element.hasUnsavedChanges);
- assert.isNotOk(element._newPreferred);
- assert.equal(element._emailsToRemove.length, 0);
- assert.equal(element._emails.length, 3);
+ assert.isNotOk(element.newPreferred);
+ assert.equal(element.emailsToRemove.length, 0);
+ assert.equal(element.emails.length, 3);
// Delete the first email and set the last as preferred.
rows[0].querySelector('gr-button')!.click();
rows[2].querySelector<HTMLInputElement>('input[type=radio]')!.click();
assert.isTrue(element.hasUnsavedChanges);
- assert.equal(element._newPreferred, 'email@three.com');
- assert.equal(element._emailsToRemove.length, 1);
- assert.equal(element._emailsToRemove[0].email, 'email@one.com');
- assert.equal(element._emails.length, 2);
+ assert.equal(element.newPreferred, 'email@three.com');
+ assert.equal(element.emailsToRemove.length, 1);
+ assert.equal(element.emailsToRemove[0].email, 'email@one.com');
+ assert.equal(element.emails.length, 2);
await element.save();
assert.equal(deleteEmailSpy.callCount, 1);
diff --git a/polygerrit-ui/app/elements/settings/gr-gpg-editor/gr-gpg-editor.ts b/polygerrit-ui/app/elements/settings/gr-gpg-editor/gr-gpg-editor.ts
index ed07fd6..3c54c59 100644
--- a/polygerrit-ui/app/elements/settings/gr-gpg-editor/gr-gpg-editor.ts
+++ b/polygerrit-ui/app/elements/settings/gr-gpg-editor/gr-gpg-editor.ts
@@ -19,24 +19,18 @@
import '../../shared/gr-button/gr-button';
import '../../shared/gr-copy-clipboard/gr-copy-clipboard';
import '../../shared/gr-overlay/gr-overlay';
-import '../../../styles/shared-styles';
-import {dom, EventApi} from '@polymer/polymer/lib/legacy/polymer.dom';
-import {PolymerElement} from '@polymer/polymer/polymer-element';
-import {htmlTemplate} from './gr-gpg-editor_html';
-import {customElement, property} from '@polymer/decorators';
import {GpgKeyInfo, GpgKeyId} from '../../../types/common';
import {GrButton} from '../../shared/gr-button/gr-button';
import {GrOverlay} from '../../shared/gr-overlay/gr-overlay';
-import {IronAutogrowTextareaElement} from '@polymer/iron-autogrow-textarea/iron-autogrow-textarea';
+import {IronAutogrowTextareaElement} from '@polymer/iron-autogrow-textarea';
import {getAppContext} from '../../../services/app-context';
-
-export interface GrGpgEditor {
- $: {
- viewKeyOverlay: GrOverlay;
- addButton: GrButton;
- newKey: IronAutogrowTextareaElement;
- };
-}
+import {css, html, LitElement} from 'lit';
+import {customElement, property, query, state} from 'lit/decorators';
+import {sharedStyles} from '../../../styles/shared-styles';
+import {formStyles} from '../../../styles/gr-form-styles';
+import {assertIsDefined} from '../../../utils/common-util';
+import {BindValueChangeEvent} from '../../../types/events';
+import {fire} from '../../../utils/event-util';
declare global {
interface HTMLElementTagNameMap {
@@ -44,35 +38,157 @@
}
}
@customElement('gr-gpg-editor')
-export class GrGpgEditor extends PolymerElement {
- static get template() {
- return htmlTemplate;
- }
+export class GrGpgEditor extends LitElement {
+ @query('#viewKeyOverlay') viewKeyOverlay?: GrOverlay;
- @property({type: Boolean, notify: true})
+ @query('#addButton') addButton?: GrButton;
+
+ @query('#newKey') newKeyTextarea?: IronAutogrowTextareaElement;
+
+ @property({type: Boolean})
hasUnsavedChanges = false;
- @property({type: Array})
- _keys: GpgKeyInfo[] = [];
+ // private but used in test
+ @state() keys: GpgKeyInfo[] = [];
- @property({type: Object})
- _keyToView?: GpgKeyInfo;
+ // private but used in test
+ @state() keyToView?: GpgKeyInfo;
- @property({type: String})
- _newKey = '';
+ // private but used in test
+ @state() newKey = '';
- @property({type: Array})
- _keysToRemove: GpgKeyInfo[] = [];
+ // private but used in test
+ @state() keysToRemove: GpgKeyInfo[] = [];
private readonly restApiService = getAppContext().restApiService;
+ static override styles = [
+ sharedStyles,
+ formStyles,
+ css`
+ .keyHeader {
+ width: 9em;
+ }
+ .userIdHeader {
+ width: 15em;
+ }
+ #viewKeyOverlay {
+ padding: var(--spacing-xxl);
+ width: 50em;
+ }
+ .closeButton {
+ bottom: 2em;
+ position: absolute;
+ right: 2em;
+ }
+ #existing {
+ margin-bottom: var(--spacing-l);
+ }
+ `,
+ ];
+
+ override render() {
+ return html`
+ <div class="gr-form-styles">
+ <fieldset id="existing">
+ <table>
+ <thead>
+ <tr>
+ <th class="idColumn">ID</th>
+ <th class="fingerPrintColumn">Fingerprint</th>
+ <th class="userIdHeader">User IDs</th>
+ <th class="keyHeader">Public Key</th>
+ <th></th>
+ <th></th>
+ </tr>
+ </thead>
+ <tbody>
+ ${this.keys.map((key, index) => this.renderKey(key, index))}
+ </tbody>
+ </table>
+ <gr-overlay id="viewKeyOverlay" with-backdrop="">
+ <fieldset>
+ <section>
+ <span class="title">Status</span>
+ <span class="value">${this.keyToView?.status}</span>
+ </section>
+ <section>
+ <span class="title">Key</span>
+ <span class="value">${this.keyToView?.key}</span>
+ </section>
+ </fieldset>
+ <gr-button
+ class="closeButton"
+ @click=${() => {
+ this.viewKeyOverlay?.close();
+ }}
+ >Close</gr-button
+ >
+ </gr-overlay>
+ <gr-button @click=${this.save} ?disabled=${!this.hasUnsavedChanges}
+ >Save changes</gr-button
+ >
+ </fieldset>
+ <fieldset>
+ <section>
+ <span class="title">New GPG key</span>
+ <span class="value">
+ <iron-autogrow-textarea
+ id="newKey"
+ autocomplete="on"
+ .bindValue=${this.newKey}
+ @bind-value-changed=${(e: BindValueChangeEvent) =>
+ this.handleNewKeyChanged(e)}
+ placeholder="New GPG Key"
+ ></iron-autogrow-textarea>
+ </span>
+ </section>
+ <gr-button
+ id="addButton"
+ ?disabled=${!this.newKey?.length}
+ @click=${this.handleAddKey}
+ >Add new GPG key</gr-button
+ >
+ </fieldset>
+ </div>
+ `;
+ }
+
+ private renderKey(key: GpgKeyInfo, index: number) {
+ return html`<tr>
+ <td class="idColumn">${key.id}</td>
+ <td class="fingerPrintColumn">${key.fingerprint}</td>
+ <td class="userIdHeader">${key.user_ids?.map(id => html`${id}`)}</td>
+ <td class="keyHeader">
+ <gr-button @click=${() => this.showKey(key)} link=""
+ >Click to View</gr-button
+ >
+ </td>
+ <td>
+ <gr-copy-clipboard
+ hasTooltip
+ buttonTitle="Copy GPG public key to clipboard"
+ hideInput
+ .text=${key.key}
+ >
+ </gr-copy-clipboard>
+ </td>
+ <td>
+ <gr-button @click=${() => this.handleDeleteKey(index)}
+ >Delete</gr-button
+ >
+ </td>
+ </tr>`;
+ }
+
+ // private but used in test
loadData() {
- this._keys = [];
+ this.keys = [];
return this.restApiService.getAccountGPGKeys().then(keys => {
if (!keys) {
return;
}
- this._keys = Object.keys(keys).map(key => {
+ this.keys = Object.keys(keys).map(key => {
const gpgKey = keys[key];
gpgKey.id = key as GpgKeyId;
return gpgKey;
@@ -80,53 +196,58 @@
});
}
+ // private but used in test
save() {
- const promises = this._keysToRemove.map(key =>
+ const promises = this.keysToRemove.map(key =>
this.restApiService.deleteAccountGPGKey(key.id!)
);
return Promise.all(promises).then(() => {
- this._keysToRemove = [];
- this.hasUnsavedChanges = false;
+ this.keysToRemove = [];
+ this.setHasUnsavedChanges(false);
});
}
- _showKey(e: Event) {
- const el = (dom(e) as EventApi).localTarget as Element;
- const index = Number(el.getAttribute('data-index')!);
- this._keyToView = this._keys[index];
- this.$.viewKeyOverlay.open();
+ private showKey(key: GpgKeyInfo) {
+ this.keyToView = key;
+ this.viewKeyOverlay?.open();
}
- _closeOverlay() {
- this.$.viewKeyOverlay.close();
+ private handleNewKeyChanged(e: BindValueChangeEvent) {
+ this.newKey = e.detail.value;
}
- _handleDeleteKey(e: Event) {
- const el = (dom(e) as EventApi).localTarget as Element;
- const index = Number(el.getAttribute('data-index')!);
- this.push('_keysToRemove', this._keys[index]);
- this.splice('_keys', index, 1);
- this.hasUnsavedChanges = true;
+ private handleDeleteKey(index: number) {
+ this.keysToRemove.push(this.keys[index]);
+ this.keys.splice(index, 1);
+ this.requestUpdate();
+ this.setHasUnsavedChanges(true);
}
- _handleAddKey() {
- this.$.addButton.disabled = true;
- this.$.newKey.disabled = true;
+ // private but used in test
+ handleAddKey() {
+ assertIsDefined(this.newKeyTextarea);
+ assertIsDefined(this.addButton);
+ this.addButton.disabled = true;
+ this.newKeyTextarea.disabled = true;
return this.restApiService
- .addAccountGPGKey({add: [this._newKey.trim()]})
+ .addAccountGPGKey({add: [this.newKey.trim()]})
.then(() => {
- this.$.newKey.disabled = false;
- this._newKey = '';
+ assertIsDefined(this.newKeyTextarea);
+ this.newKeyTextarea.disabled = false;
+ this.newKey = '';
this.loadData();
})
.catch(() => {
- this.$.addButton.disabled = false;
- this.$.newKey.disabled = false;
+ assertIsDefined(this.newKeyTextarea);
+ assertIsDefined(this.addButton);
+ this.addButton.disabled = false;
+ this.newKeyTextarea.disabled = false;
});
}
- _computeAddButtonDisabled(newKey: string) {
- return !newKey.length;
+ private setHasUnsavedChanges(value: boolean) {
+ this.hasUnsavedChanges = value;
+ fire(this, 'has-unsaved-changes-changed', {value});
}
}
diff --git a/polygerrit-ui/app/elements/settings/gr-gpg-editor/gr-gpg-editor_html.ts b/polygerrit-ui/app/elements/settings/gr-gpg-editor/gr-gpg-editor_html.ts
deleted file mode 100644
index f4641c2..0000000
--- a/polygerrit-ui/app/elements/settings/gr-gpg-editor/gr-gpg-editor_html.ts
+++ /dev/null
@@ -1,128 +0,0 @@
-/**
- * @license
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-import {html} from '@polymer/polymer/lib/utils/html-tag';
-
-export const htmlTemplate = html`
- <style include="shared-styles">
- /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
- </style>
- <style include="gr-form-styles">
- .keyHeader {
- width: 9em;
- }
- .userIdHeader {
- width: 15em;
- }
- #viewKeyOverlay {
- padding: var(--spacing-xxl);
- width: 50em;
- }
- .closeButton {
- bottom: 2em;
- position: absolute;
- right: 2em;
- }
- #existing {
- margin-bottom: var(--spacing-l);
- }
- </style>
- <div class="gr-form-styles">
- <fieldset id="existing">
- <table>
- <thead>
- <tr>
- <th class="idColumn">ID</th>
- <th class="fingerPrintColumn">Fingerprint</th>
- <th class="userIdHeader">User IDs</th>
- <th class="keyHeader">Public Key</th>
- <th></th>
- <th></th>
- </tr>
- </thead>
- <tbody>
- <template is="dom-repeat" items="[[_keys]]" as="key">
- <tr>
- <td class="idColumn">[[key.id]]</td>
- <td class="fingerPrintColumn">[[key.fingerprint]]</td>
- <td class="userIdHeader">
- <template is="dom-repeat" items="[[key.user_ids]]">
- [[item]]
- </template>
- </td>
- <td class="keyHeader">
- <gr-button on-click="_showKey" data-index$="[[index]]" link=""
- >Click to View</gr-button
- >
- </td>
- <td>
- <gr-copy-clipboard
- hasTooltip=""
- buttonTitle="Copy GPG public key to clipboard"
- hideInput=""
- text="[[key.key]]"
- >
- </gr-copy-clipboard>
- </td>
- <td>
- <gr-button data-index$="[[index]]" on-click="_handleDeleteKey"
- >Delete</gr-button
- >
- </td>
- </tr>
- </template>
- </tbody>
- </table>
- <gr-overlay id="viewKeyOverlay" with-backdrop="">
- <fieldset>
- <section>
- <span class="title">Status</span>
- <span class="value">[[_keyToView.status]]</span>
- </section>
- <section>
- <span class="title">Key</span>
- <span class="value">[[_keyToView.key]]</span>
- </section>
- </fieldset>
- <gr-button class="closeButton" on-click="_closeOverlay"
- >Close</gr-button
- >
- </gr-overlay>
- <gr-button on-click="save" disabled$="[[!hasUnsavedChanges]]"
- >Save changes</gr-button
- >
- </fieldset>
- <fieldset>
- <section>
- <span class="title">New GPG key</span>
- <span class="value">
- <iron-autogrow-textarea
- id="newKey"
- autocomplete="on"
- bind-value="{{_newKey}}"
- placeholder="New GPG Key"
- ></iron-autogrow-textarea>
- </span>
- </section>
- <gr-button
- id="addButton"
- disabled$="[[_computeAddButtonDisabled(_newKey)]]"
- on-click="_handleAddKey"
- >Add new GPG key</gr-button
- >
- </fieldset>
- </div>
-`;
diff --git a/polygerrit-ui/app/elements/settings/gr-gpg-editor/gr-gpg-editor_test.ts b/polygerrit-ui/app/elements/settings/gr-gpg-editor/gr-gpg-editor_test.ts
index c31b40d..0f775b80 100644
--- a/polygerrit-ui/app/elements/settings/gr-gpg-editor/gr-gpg-editor_test.ts
+++ b/polygerrit-ui/app/elements/settings/gr-gpg-editor/gr-gpg-editor_test.ts
@@ -31,7 +31,6 @@
OpenPgpUserIds,
} from '../../../api/rest-api';
import {GrButton} from '../../shared/gr-button/gr-button';
-import * as MockInteractions from '@polymer/iron-test-helpers/mock-interactions';
const basicFixture = fixtureFromElement('gr-gpg-editor');
@@ -41,9 +40,9 @@
setup(async () => {
const fingerprint1 =
- '0192 723D 42D1 0C5B 32A6 E1E0 9350 9E4B AFC8 A49B' as GpgKeyFingerprint;
+ '0192 723D 42D1 0C5B 32A6 E1E0 9350 9E4B AFC8 A49B' as GpgKeyFingerprint;
const fingerprint2 =
- '0196 723D 42D1 0C5B 32A6 E1E0 9350 9E4B AFC8 A49B' as GpgKeyFingerprint;
+ '0196 723D 42D1 0C5B 32A6 E1E0 9350 9E4B AFC8 A49B' as GpgKeyFingerprint;
keys = {
AFC8A49B: {
fingerprint: fingerprint1,
@@ -70,7 +69,138 @@
element = basicFixture.instantiate();
await element.loadData();
- await flush();
+ await element.updateComplete;
+ });
+
+ test('renders', () => {
+ expect(element).shadowDom.to.equal(/* HTML */ `<div class="gr-form-styles">
+ <fieldset id="existing">
+ <table>
+ <thead>
+ <tr>
+ <th class="idColumn">ID</th>
+ <th class="fingerPrintColumn">Fingerprint</th>
+ <th class="userIdHeader">User IDs</th>
+ <th class="keyHeader">Public Key</th>
+ <th></th>
+ <th></th>
+ </tr>
+ </thead>
+ <tbody>
+ <tr>
+ <td class="idColumn">AFC8A49B</td>
+ <td class="fingerPrintColumn">
+ 0192 723D 42D1 0C5B 32A6 E1E0 9350 9E4B AFC8 A49B
+ </td>
+ <td class="userIdHeader">John Doe john.doe@example.com</td>
+ <td class="keyHeader">
+ <gr-button
+ aria-disabled="false"
+ link=""
+ role="button"
+ tabindex="0"
+ >
+ Click to View
+ </gr-button>
+ </td>
+ <td>
+ <gr-copy-clipboard
+ buttontitle="Copy GPG public key to clipboard"
+ hastooltip=""
+ hideinput=""
+ >
+ </gr-copy-clipboard>
+ </td>
+ <td>
+ <gr-button aria-disabled="false" role="button" tabindex="0">
+ Delete
+ </gr-button>
+ </td>
+ </tr>
+ <tr>
+ <td class="idColumn">AED9B59C</td>
+ <td class="fingerPrintColumn">
+ 0196 723D 42D1 0C5B 32A6 E1E0 9350 9E4B AFC8 A49B
+ </td>
+ <td class="userIdHeader">Gerrit gerrit@example.com</td>
+ <td class="keyHeader">
+ <gr-button
+ aria-disabled="false"
+ link=""
+ role="button"
+ tabindex="0"
+ >
+ Click to View
+ </gr-button>
+ </td>
+ <td>
+ <gr-copy-clipboard
+ buttontitle="Copy GPG public key to clipboard"
+ hastooltip=""
+ hideinput=""
+ >
+ </gr-copy-clipboard>
+ </td>
+ <td>
+ <gr-button aria-disabled="false" role="button" tabindex="0">
+ Delete
+ </gr-button>
+ </td>
+ </tr>
+ </tbody>
+ </table>
+ <gr-overlay
+ aria-hidden="true"
+ id="viewKeyOverlay"
+ style="outline: none; display: none;"
+ tabindex="-1"
+ with-backdrop=""
+ >
+ <fieldset>
+ <section>
+ <span class="title"> Status </span> <span class="value"> </span>
+ </section>
+ <section>
+ <span class="title"> Key </span> <span class="value"> </span>
+ </section>
+ </fieldset>
+ <gr-button
+ aria-disabled="false"
+ class="closeButton"
+ role="button"
+ tabindex="0"
+ >
+ Close
+ </gr-button>
+ </gr-overlay>
+ <gr-button aria-disabled="true" disabled="" role="button" tabindex="-1">
+ Save changes
+ </gr-button>
+ </fieldset>
+ <fieldset>
+ <section>
+ <span class="title"> New GPG key </span>
+ <span class="value">
+ <iron-autogrow-textarea
+ aria-disabled="false"
+ autocomplete="on"
+ id="newKey"
+ placeholder="New GPG Key"
+ >
+ </iron-autogrow-textarea>
+ </span>
+ </section>
+ <gr-button
+ aria-disabled="true"
+ disabled=""
+ id="addButton"
+ role="button"
+ tabindex="-1"
+ >
+ Add new GPG key
+ </gr-button>
+ </fieldset>
+ </div> `);
});
test('renders', () => {
@@ -92,7 +222,7 @@
Promise.resolve(new Response())
);
- assert.equal(element._keysToRemove.length, 0);
+ assert.equal(element.keysToRemove.length, 0);
assert.isFalse(element.hasUnsavedChanges);
// Get the delete button for the last row.
@@ -101,23 +231,23 @@
'tbody tr:last-of-type td:nth-child(6) gr-button'
);
- MockInteractions.tap(button);
+ button.click();
- assert.equal(element._keys.length, 1);
- assert.equal(element._keysToRemove.length, 1);
- assert.equal(element._keysToRemove[0], lastKey);
+ assert.equal(element.keys.length, 1);
+ assert.equal(element.keysToRemove.length, 1);
+ assert.equal(element.keysToRemove[0], lastKey);
assert.isTrue(element.hasUnsavedChanges);
assert.isFalse(saveStub.called);
await element.save();
assert.isTrue(saveStub.called);
assert.equal(saveStub.lastCall.args[0], Object.keys(keys)[1]);
- assert.equal(element._keysToRemove.length, 0);
+ assert.equal(element.keysToRemove.length, 0);
assert.isFalse(element.hasUnsavedChanges);
});
test('show key', () => {
- const openSpy = sinon.spy(element.$.viewKeyOverlay, 'open');
+ const openSpy = sinon.spy(element.viewKeyOverlay!, 'open');
// Get the show button for the last row.
const button = queryAndAssert<GrButton>(
@@ -125,16 +255,14 @@
'tbody tr:last-of-type td:nth-child(4) gr-button'
);
- MockInteractions.tap(button);
-
- assert.equal(element._keyToView, keys[Object.keys(keys)[1]]);
+ button.click();
+ assert.equal(element.keyToView, keys[Object.keys(keys)[1]]);
assert.isTrue(openSpy.called);
});
test('add key', async () => {
const newKeyString =
- '-----BEGIN PGP PUBLIC KEY BLOCK-----' +
- '\nVersion: BCPG v1.52\n\t<key 3>';
+ '-----BEGIN PGP PUBLIC KEY BLOCK-----' + ' Version: BCPG v1.52 \t<key 3>';
const newKeyObject = {
ADE8A59B: {
fingerprint:
@@ -150,21 +278,22 @@
Promise.resolve(newKeyObject)
);
- element._newKey = newKeyString;
+ element.newKey = newKeyString;
+ await element.updateComplete;
- assert.isFalse(element.$.addButton.disabled);
- assert.isFalse(element.$.newKey.disabled);
+ assert.isFalse(element.addButton!.disabled);
+ assert.isFalse(element.newKeyTextarea!.disabled);
const promise = mockPromise();
- element._handleAddKey().then(() => {
- assert.isTrue(element.$.addButton.disabled);
- assert.isFalse(element.$.newKey.disabled);
- assert.equal(element._keys.length, 2);
+ element.handleAddKey().then(() => {
+ assert.isTrue(element.addButton!.disabled);
+ assert.isFalse(element.newKeyTextarea!.disabled);
+ assert.equal(element.keys.length, 2);
promise.resolve();
});
- assert.isTrue(element.$.addButton.disabled);
- assert.isTrue(element.$.newKey.disabled);
+ assert.isTrue(element.addButton!.disabled);
+ assert.isTrue(element.newKeyTextarea!.disabled);
assert.isTrue(addStub.called);
assert.deepEqual(addStub.lastCall.args[0], {add: [newKeyString]});
@@ -178,21 +307,22 @@
Promise.reject(new Error('error'))
);
- element._newKey = newKeyString;
+ element.newKey = newKeyString;
+ await element.updateComplete;
- assert.isFalse(element.$.addButton.disabled);
- assert.isFalse(element.$.newKey.disabled);
+ assert.isFalse(element.addButton!.disabled);
+ assert.isFalse(element.newKeyTextarea!.disabled);
const promise = mockPromise();
- element._handleAddKey().then(() => {
- assert.isFalse(element.$.addButton.disabled);
- assert.isFalse(element.$.newKey.disabled);
- assert.equal(element._keys.length, 2);
+ element.handleAddKey().then(() => {
+ assert.isFalse(element.addButton!.disabled);
+ assert.isFalse(element.newKeyTextarea!.disabled);
+ assert.equal(element.keys.length, 2);
promise.resolve();
});
- assert.isTrue(element.$.addButton.disabled);
- assert.isTrue(element.$.newKey.disabled);
+ assert.isTrue(element.addButton!.disabled);
+ assert.isTrue(element.newKeyTextarea!.disabled);
assert.isTrue(addStub.called);
assert.deepEqual(addStub.lastCall.args[0], {add: [newKeyString]});
diff --git a/polygerrit-ui/app/elements/settings/gr-group-list/gr-group-list.ts b/polygerrit-ui/app/elements/settings/gr-group-list/gr-group-list.ts
index 2db8752..2b8a1e9 100644
--- a/polygerrit-ui/app/elements/settings/gr-group-list/gr-group-list.ts
+++ b/polygerrit-ui/app/elements/settings/gr-group-list/gr-group-list.ts
@@ -80,7 +80,7 @@
return html`
<tr>
<td class="nameColumn">
- <a href="${href}"> ${group.name} </a>
+ <a href=${href}> ${group.name} </a>
</td>
<td>${group.description}</td>
<td class="visibleCell">
diff --git a/polygerrit-ui/app/elements/settings/gr-http-password/gr-http-password.ts b/polygerrit-ui/app/elements/settings/gr-http-password/gr-http-password.ts
index ebe30a3..73fc55f 100644
--- a/polygerrit-ui/app/elements/settings/gr-http-password/gr-http-password.ts
+++ b/polygerrit-ui/app/elements/settings/gr-http-password/gr-http-password.ts
@@ -125,7 +125,7 @@
>
</div>
<span ?hidden=${!this._passwordUrl}>
- <a href="${this._passwordUrl!}" target="_blank" rel="noopener">
+ <a href=${this._passwordUrl!} target="_blank" rel="noopener">
Obtain password</a
>
(opens in a new tab)
@@ -144,7 +144,7 @@
hasTooltip=""
buttonTitle="Copy password to clipboard"
hideInput=""
- .text="${this._generatedPassword}"
+ .text=${this._generatedPassword}
>
</gr-copy-clipboard>
</section>
diff --git a/polygerrit-ui/app/elements/settings/gr-identities/gr-identities.ts b/polygerrit-ui/app/elements/settings/gr-identities/gr-identities.ts
index 9e1aaa9..d8a7579 100644
--- a/polygerrit-ui/app/elements/settings/gr-identities/gr-identities.ts
+++ b/polygerrit-ui/app/elements/settings/gr-identities/gr-identities.ts
@@ -14,105 +14,185 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-import '../../../styles/shared-styles';
-import '../../../styles/gr-form-styles';
import '../../admin/gr-confirm-delete-item-dialog/gr-confirm-delete-item-dialog';
import '../../shared/gr-button/gr-button';
import '../../shared/gr-overlay/gr-overlay';
-import {PolymerElement} from '@polymer/polymer/polymer-element';
-import {htmlTemplate} from './gr-identities_html';
import {getBaseUrl} from '../../../utils/url-util';
-import {customElement, property} from '@polymer/decorators';
import {AccountExternalIdInfo, ServerInfo} from '../../../types/common';
import {GrOverlay} from '../../shared/gr-overlay/gr-overlay';
-import {PolymerDomRepeatEvent} from '../../../types/types';
import {getAppContext} from '../../../services/app-context';
import {AuthType} from '../../../constants/constants';
+import {LitElement, css, html, PropertyValues} from 'lit';
+import {customElement, property, query, state} from 'lit/decorators';
+import {sharedStyles} from '../../../styles/shared-styles';
+import {formStyles} from '../../../styles/gr-form-styles';
+import {classMap} from 'lit/directives/class-map';
+import {when} from 'lit/directives/when.js';
+import {assertIsDefined} from '../../../utils/common-util';
const AUTH = [AuthType.OPENID, AuthType.OAUTH];
-export interface GrIdentities {
- $: {
- overlay: GrOverlay;
- };
-}
-
@customElement('gr-identities')
-export class GrIdentities extends PolymerElement {
- static get template() {
- return htmlTemplate;
- }
+export class GrIdentities extends LitElement {
+ @query('#overlay') overlay?: GrOverlay;
- @property({type: Array})
- _identities: AccountExternalIdInfo[] = [];
+ @state() private identities: AccountExternalIdInfo[] = [];
- @property({type: String})
- _idName?: string;
+ // temporary var for communicating with the confirmation dialog
+ // private but used in test
+ @state() idName?: string;
- @property({type: Object})
- serverConfig?: ServerInfo;
+ @property({type: Object}) serverConfig?: ServerInfo;
- @property({
- type: Boolean,
- computed: '_computeShowLinkAnotherIdentity(serverConfig)',
- })
- _showLinkAnotherIdentity?: boolean;
+ @state() showLinkAnotherIdentity = false;
private readonly restApiService = getAppContext().restApiService;
+ static override styles = [
+ sharedStyles,
+ formStyles,
+ css`
+ tr th.emailAddressHeader,
+ tr th.identityHeader {
+ width: 15em;
+ padding: 0 10px;
+ }
+ tr td.statusColumn,
+ tr td.emailAddressColumn,
+ tr td.identityColumn {
+ word-break: break-word;
+ }
+ tr td.emailAddressColumn,
+ tr td.identityColumn {
+ padding: 4px 10px;
+ width: 15em;
+ }
+ .deleteButton {
+ float: right;
+ }
+ .deleteButton:not(.show) {
+ display: none;
+ }
+ .space {
+ margin-bottom: var(--spacing-l);
+ }
+ `,
+ ];
+
+ override render() {
+ return html`<div class="gr-form-styles">
+ <fieldset class="space">
+ <table>
+ <thead>
+ <tr>
+ <th class="statusHeader">Status</th>
+ <th class="emailAddressHeader">Email Address</th>
+ <th class="identityHeader">Identity</th>
+ <th class="deleteHeader"></th>
+ </tr>
+ </thead>
+ <tbody>
+ ${this.getIdentities().map((account, index) =>
+ this.renderIdentity(account, index)
+ )}
+ </tbody>
+ </table>
+ </fieldset>
+ ${when(
+ this.showLinkAnotherIdentity,
+ () => html`<fieldset>
+ <a href=${this.computeLinkAnotherIdentity()}>
+ <gr-button id="linkAnotherIdentity" link=""
+ >Link Another Identity</gr-button
+ >
+ </a>
+ </fieldset>`
+ )}
+ </div>
+ <gr-overlay id="overlay" with-backdrop>
+ <gr-confirm-delete-item-dialog
+ class="confirmDialog"
+ @confirm=${this.handleDeleteItemConfirm}
+ @cancel=${this.handleConfirmDialogCancel}
+ .item=${this.idName}
+ itemtypename="ID"
+ ></gr-confirm-delete-item-dialog>
+ </gr-overlay>`;
+ }
+
+ private renderIdentity(account: AccountExternalIdInfo, index: number) {
+ return html`<tr>
+ <td class="statusColumn">${account.trusted ? '' : 'Untrusted'}</td>
+ <td class="emailAddressColumn">${account.email_address}</td>
+ <td class="identityColumn">
+ ${account.identity.startsWith('mailto:') ? '' : account.identity}
+ </td>
+ <td class="deleteColumn">
+ <gr-button
+ data-index=${index}
+ class=${classMap({
+ deleteButton: true,
+ show: !!account.can_delete,
+ })}
+ @click=${() => this.handleDeleteItem(account.identity)}
+ >
+ Delete
+ </gr-button>
+ </td>
+ </tr>`;
+ }
+
+ override willUpdate(changedProperties: PropertyValues) {
+ if (changedProperties.has('serverConfig')) {
+ this.showLinkAnotherIdentity = this.computeShowLinkAnotherIdentity();
+ }
+ }
+
+ // private but used in test
+ getIdentities() {
+ return this.identities.filter(
+ account => !account.identity.startsWith('username:')
+ );
+ }
+
loadData() {
- return this.restApiService.getExternalIds().then(id => {
- this._identities = id ?? [];
+ return this.restApiService.getExternalIds().then(ids => {
+ this.identities = ids ?? [];
});
}
- _computeIdentity(id: string) {
- return id && id.startsWith('mailto:') ? '' : id;
- }
-
+ // private but used in test
_computeHideDeleteClass(canDelete?: boolean) {
return canDelete ? 'show' : '';
}
- _handleDeleteItemConfirm() {
- this.$.overlay.close();
- return this.restApiService
- .deleteAccountIdentity([this._idName!])
- .then(() => {
- this.loadData();
- });
+ handleDeleteItemConfirm() {
+ this.overlay?.close();
+ assertIsDefined(this.idName);
+ return this.restApiService.deleteAccountIdentity([this.idName]).then(() => {
+ this.loadData();
+ });
}
- _handleConfirmDialogCancel() {
- this.$.overlay.close();
+ private handleConfirmDialogCancel() {
+ this.overlay?.close();
}
- _handleDeleteItem(e: PolymerDomRepeatEvent<AccountExternalIdInfo>) {
- const name = e.model.item.identity;
- if (!name) {
- return;
- }
- this._idName = name;
- this.$.overlay.open();
+ private handleDeleteItem(name: string) {
+ this.idName = name;
+ this.overlay?.open();
}
- _computeIsTrusted(item?: boolean) {
- return item ? '' : 'Untrusted';
- }
-
- filterIdentities(item: AccountExternalIdInfo) {
- return !item.identity.startsWith('username:');
- }
-
- _computeShowLinkAnotherIdentity(config?: ServerInfo) {
- if (config?.auth?.auth_type) {
- return AUTH.includes(config.auth.auth_type);
+ // private but used in test
+ computeShowLinkAnotherIdentity() {
+ if (this.serverConfig?.auth?.auth_type) {
+ return AUTH.includes(this.serverConfig.auth.auth_type);
}
return false;
}
- _computeLinkAnotherIdentity() {
+ private computeLinkAnotherIdentity() {
const baseUrl = getBaseUrl() || '';
let pathname = window.location.pathname;
if (baseUrl) {
diff --git a/polygerrit-ui/app/elements/settings/gr-identities/gr-identities_html.ts b/polygerrit-ui/app/elements/settings/gr-identities/gr-identities_html.ts
deleted file mode 100644
index a30840c..0000000
--- a/polygerrit-ui/app/elements/settings/gr-identities/gr-identities_html.ts
+++ /dev/null
@@ -1,104 +0,0 @@
-/**
- * @license
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-import {html} from '@polymer/polymer/lib/utils/html-tag';
-
-export const htmlTemplate = html`
- <style include="shared-styles">
- /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
- </style>
- <style include="gr-form-styles">
- tr th.emailAddressHeader,
- tr th.identityHeader {
- width: 15em;
- padding: 0 10px;
- }
- tr td.statusColumn,
- tr td.emailAddressColumn,
- tr td.identityColumn {
- word-break: break-word;
- }
- tr td.emailAddressColumn,
- tr td.identityColumn {
- padding: 4px 10px;
- width: 15em;
- }
- .deleteButton {
- float: right;
- }
- .deleteButton:not(.show) {
- display: none;
- }
- .space {
- margin-bottom: var(--spacing-l);
- }
- </style>
- <div class="gr-form-styles">
- <fieldset class="space">
- <table>
- <thead>
- <tr>
- <th class="statusHeader">Status</th>
- <th class="emailAddressHeader">Email Address</th>
- <th class="identityHeader">Identity</th>
- <th class="deleteHeader"></th>
- </tr>
- </thead>
- <tbody>
- <template
- is="dom-repeat"
- items="[[_identities]]"
- filter="filterIdentities"
- >
- <tr>
- <td class="statusColumn">[[_computeIsTrusted(item.trusted)]]</td>
- <td class="emailAddressColumn">[[item.email_address]]</td>
- <td class="identityColumn">
- [[_computeIdentity(item.identity)]]
- </td>
- <td class="deleteColumn">
- <gr-button
- class$="deleteButton [[_computeHideDeleteClass(item.can_delete)]]"
- on-click="_handleDeleteItem"
- >
- Delete
- </gr-button>
- </td>
- </tr>
- </template>
- </tbody>
- </table>
- </fieldset>
- <template is="dom-if" if="[[_showLinkAnotherIdentity]]">
- <fieldset>
- <a href$="[[_computeLinkAnotherIdentity()]]">
- <gr-button id="linkAnotherIdentity" link=""
- >Link Another Identity</gr-button
- >
- </a>
- </fieldset>
- </template>
- </div>
- <gr-overlay id="overlay" with-backdrop="">
- <gr-confirm-delete-item-dialog
- class="confirmDialog"
- on-confirm="_handleDeleteItemConfirm"
- on-cancel="_handleConfirmDialogCancel"
- item="[[_idName]]"
- itemTypeName="ID"
- ></gr-confirm-delete-item-dialog>
- </gr-overlay>
-`;
diff --git a/polygerrit-ui/app/elements/settings/gr-identities/gr-identities_test.ts b/polygerrit-ui/app/elements/settings/gr-identities/gr-identities_test.ts
index ecc322ef7..8ee84bf 100644
--- a/polygerrit-ui/app/elements/settings/gr-identities/gr-identities_test.ts
+++ b/polygerrit-ui/app/elements/settings/gr-identities/gr-identities_test.ts
@@ -23,9 +23,8 @@
import {ServerInfo} from '../../../types/common';
import {createServerInfo} from '../../../test/test-data-generators';
import {queryAll, queryAndAssert} from '../../../test/test-utils';
-import * as MockInteractions from '@polymer/iron-test-helpers/mock-interactions';
-
-const basicFixture = fixtureFromElement('gr-identities');
+import {GrButton} from '../../shared/gr-button/gr-button';
+import {fixture, html} from '@open-wc/testing-helpers';
suite('gr-identities tests', () => {
let element: GrIdentities;
@@ -51,9 +50,72 @@
setup(async () => {
stubRestApi('getExternalIds').returns(Promise.resolve(ids));
- element = basicFixture.instantiate();
+ element = await fixture<GrIdentities>(
+ html`<gr-identities></gr-identities>`
+ );
await element.loadData();
- await flush();
+ await element.updateComplete;
+ });
+
+ test('renders', () => {
+ expect(element).shadowDom.to.equal(/* HTML */ `<div class="gr-form-styles">
+ <fieldset class="space">
+ <table>
+ <thead>
+ <tr>
+ <th class="statusHeader">Status</th>
+ <th class="emailAddressHeader">Email Address</th>
+ <th class="identityHeader">Identity</th>
+ <th class="deleteHeader"></th>
+ </tr>
+ </thead>
+ <tbody>
+ <tr>
+ <td class="statusColumn">Untrusted</td>
+ <td class="emailAddressColumn">gerrit@example.com</td>
+ <td class="identityColumn">gerrit:gerrit</td>
+ <td class="deleteColumn">
+ <gr-button
+ aria-disabled="false"
+ class="deleteButton"
+ data-index="0"
+ role="button"
+ tabindex="0"
+ >
+ Delete
+ </gr-button>
+ </td>
+ </tr>
+ <tr>
+ <td class="statusColumn"></td>
+ <td class="emailAddressColumn">gerrit2@example.com</td>
+ <td class="identityColumn"></td>
+ <td class="deleteColumn">
+ <gr-button
+ aria-disabled="false"
+ class="deleteButton show"
+ data-index="1"
+ role="button"
+ tabindex="0"
+ >
+ Delete
+ </gr-button>
+ </td>
+ </tr>
+ </tbody>
+ </table>
+ </fieldset>
+ </div>
+ <gr-overlay
+ aria-hidden="true"
+ id="overlay"
+ style="outline: none; display: none;"
+ tabindex="-1"
+ with-backdrop=""
+ >
+ <gr-confirm-delete-item-dialog class="confirmDialog" itemtypename="ID">
+ </gr-confirm-delete-item-dialog
+ ></gr-overlay>`);
});
test('renders', () => {
@@ -78,70 +140,71 @@
assert.equal(nameCells[1]!, 'gerrit2@example.com');
});
- test('_computeIdentity', () => {
- assert.equal(element._computeIdentity(ids[0].identity), 'username:john');
- assert.equal(element._computeIdentity(ids[2].identity), '');
- });
-
test('filterIdentities', () => {
- assert.isFalse(element.filterIdentities(ids[0]));
-
- assert.isTrue(element.filterIdentities(ids[1]));
+ assert.notInclude(element.getIdentities(), ids[0]);
+ assert.include(element.getIdentities(), ids[1]);
});
test('delete id', async () => {
- element._idName = 'mailto:gerrit2@example.com';
+ element.idName = 'mailto:gerrit2@example.com';
const loadDataStub = sinon.stub(element, 'loadData');
- await element._handleDeleteItemConfirm();
+ await element.handleDeleteItemConfirm();
assert.isTrue(loadDataStub.called);
});
- test('_handleDeleteItem opens modal', () => {
- const deleteBtn = queryAndAssert(element, '.deleteButton');
- const deleteItem = sinon.stub(element, '_handleDeleteItem');
- MockInteractions.tap(deleteBtn);
- assert.isTrue(deleteItem.called);
+ test('handleDeleteItem opens modal', async () => {
+ const deleteBtn = queryAndAssert<GrButton>(element, '.deleteButton');
+ deleteBtn.click();
+ await element.updateComplete;
+ assert.isTrue(element.overlay?.opened);
});
- test('_computeShowLinkAnotherIdentity', () => {
+ test('computeShowLinkAnotherIdentity', () => {
const config: ServerInfo = {
...createServerInfo(),
};
config.auth.auth_type = AuthType.OAUTH;
- assert.isTrue(element._computeShowLinkAnotherIdentity(config));
+ element.serverConfig = config;
+ assert.isTrue(element.computeShowLinkAnotherIdentity());
config.auth.auth_type = AuthType.OPENID;
- assert.isTrue(element._computeShowLinkAnotherIdentity(config));
+ element.serverConfig = config;
+ assert.isTrue(element.computeShowLinkAnotherIdentity());
config.auth.auth_type = AuthType.HTTP_LDAP;
- assert.isFalse(element._computeShowLinkAnotherIdentity(config));
+ element.serverConfig = config;
+ assert.isFalse(element.computeShowLinkAnotherIdentity());
config.auth.auth_type = AuthType.LDAP;
- assert.isFalse(element._computeShowLinkAnotherIdentity(config));
+ element.serverConfig = config;
+ assert.isFalse(element.computeShowLinkAnotherIdentity());
config.auth.auth_type = AuthType.HTTP;
- assert.isFalse(element._computeShowLinkAnotherIdentity(config));
+ element.serverConfig = config;
+ assert.isFalse(element.computeShowLinkAnotherIdentity());
- assert.isFalse(element._computeShowLinkAnotherIdentity(undefined));
+ element.serverConfig = undefined;
+ assert.isFalse(element.computeShowLinkAnotherIdentity());
});
- test('_showLinkAnotherIdentity', () => {
+ test('showLinkAnotherIdentity', async () => {
let config: ServerInfo = {
...createServerInfo(),
};
config.auth.auth_type = AuthType.OAUTH;
-
element.serverConfig = config;
+ await element.updateComplete;
- assert.isTrue(element._showLinkAnotherIdentity);
+ assert.isTrue(element.showLinkAnotherIdentity);
config = {
...createServerInfo(),
};
config.auth.auth_type = AuthType.LDAP;
element.serverConfig = config;
+ await element.updateComplete;
- assert.isFalse(element._showLinkAnotherIdentity);
+ assert.isFalse(element.showLinkAnotherIdentity);
});
});
diff --git a/polygerrit-ui/app/elements/settings/gr-menu-editor/gr-menu-editor.ts b/polygerrit-ui/app/elements/settings/gr-menu-editor/gr-menu-editor.ts
index c392a13..845b30c 100644
--- a/polygerrit-ui/app/elements/settings/gr-menu-editor/gr-menu-editor.ts
+++ b/polygerrit-ui/app/elements/settings/gr-menu-editor/gr-menu-editor.ts
@@ -1,98 +1,235 @@
/**
* @license
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
+ * Copyright 2016 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
*/
import '@polymer/iron-input/iron-input';
import '../../shared/gr-button/gr-button';
-import '../../../styles/shared-styles';
-import '../../../styles/gr-form-styles';
-import {dom, EventApi} from '@polymer/polymer/lib/legacy/polymer.dom';
-import {PolymerElement} from '@polymer/polymer/polymer-element';
-import {htmlTemplate} from './gr-menu-editor_html';
-import {customElement, property} from '@polymer/decorators';
-import {TopMenuItemInfo} from '../../../types/common';
+import {PreferencesInfo, TopMenuItemInfo} from '../../../types/common';
+import {css, html, LitElement} from 'lit';
+import {sharedStyles} from '../../../styles/shared-styles';
+import {formStyles} from '../../../styles/gr-form-styles';
+import {state, customElement} from 'lit/decorators';
+import {BindValueChangeEvent} from '../../../types/events';
+import {subscribe} from '../../lit/subscription-controller';
+import {getAppContext} from '../../../services/app-context';
+import {deepEqual} from '../../../utils/deep-util';
+import {createDefaultPreferences} from '../../../constants/constants';
+import {fontStyles} from '../../../styles/gr-font-styles';
+import {classMap} from 'lit/directives/class-map';
+import {menuPageStyles} from '../../../styles/gr-menu-page-styles';
@customElement('gr-menu-editor')
-export class GrMenuEditor extends PolymerElement {
- static get template() {
- return htmlTemplate;
+export class GrMenuEditor extends LitElement {
+ @state()
+ menuItems: TopMenuItemInfo[] = [];
+
+ @state()
+ originalPrefs: PreferencesInfo = createDefaultPreferences();
+
+ @state()
+ newName = '';
+
+ @state()
+ newUrl = '';
+
+ private readonly userModel = getAppContext().userModel;
+
+ override connectedCallback() {
+ super.connectedCallback();
+ subscribe(this, this.userModel.preferences$, prefs => {
+ this.originalPrefs = prefs;
+ this.menuItems = [...prefs.my];
+ });
}
- @property({type: Array})
- menuItems!: TopMenuItemInfo[];
+ static override styles = [
+ formStyles,
+ sharedStyles,
+ fontStyles,
+ menuPageStyles,
+ css`
+ .buttonColumn {
+ width: 2em;
+ }
+ .moveUpButton,
+ .moveDownButton {
+ width: 100%;
+ }
+ tbody tr:first-of-type td .moveUpButton,
+ tbody tr:last-of-type td .moveDownButton {
+ display: none;
+ }
+ td.urlCell {
+ word-break: break-word;
+ }
+ .newUrlInput {
+ min-width: 23em;
+ }
+ `,
+ ];
- @property({type: String})
- _newName?: string;
-
- @property({type: String})
- _newUrl?: string;
-
- _handleMoveUpButton(e: Event) {
- const target = (dom(e) as EventApi).localTarget;
- if (!(target instanceof HTMLElement)) return;
- const index = Number(target.dataset['index']);
- if (index === 0) {
- return;
- }
- const row = this.menuItems[index];
- const prev = this.menuItems[index - 1];
- this.splice('menuItems', index - 1, 2, row, prev);
+ override render() {
+ const unchanged = deepEqual(this.menuItems, this.originalPrefs.my);
+ const classes = {
+ 'heading-2': true,
+ edited: !unchanged,
+ };
+ return html`
+ <div class="gr-form-styles">
+ <h2 id="Menu" class=${classMap(classes)}>Menu</h2>
+ <fieldset id="menu">
+ <table>
+ <thead>
+ <tr>
+ <th>Name</th>
+ <th>URL</th>
+ </tr>
+ </thead>
+ <tbody>
+ ${this.menuItems.map((item, index) =>
+ this.renderMenuItemRow(item, index)
+ )}
+ </tbody>
+ <tfoot>
+ ${this.renderFooterRow()}
+ </tfoot>
+ </table>
+ <gr-button id="save" @click=${this.handleSave} ?disabled=${unchanged}
+ >Save changes</gr-button
+ >
+ <gr-button id="reset" link @click=${this.handleReset}
+ >Reset</gr-button
+ >
+ </fieldset>
+ </div>
+ `;
}
- _handleMoveDownButton(e: Event) {
- const target = (dom(e) as EventApi).localTarget;
- if (!(target instanceof HTMLElement)) return;
- const index = Number(target.dataset['index']);
- if (index === this.menuItems.length - 1) {
- return;
- }
- const row = this.menuItems[index];
- const next = this.menuItems[index + 1];
- this.splice('menuItems', index, 2, next, row);
+ private renderMenuItemRow(item: TopMenuItemInfo, index: number) {
+ return html`
+ <tr>
+ <td>${item.name}</td>
+ <td class="urlCell">${item.url}</td>
+ <td class="buttonColumn">
+ <gr-button
+ link
+ data-index=${index}
+ @click=${() => this.swapItems(index, index - 1)}
+ class="moveUpButton"
+ >↑</gr-button
+ >
+ </td>
+ <td class="buttonColumn">
+ <gr-button
+ link
+ data-index=${index}
+ @click=${() => this.swapItems(index, index + 1)}
+ class="moveDownButton"
+ >↓</gr-button
+ >
+ </td>
+ <td>
+ <gr-button
+ link
+ data-index=${index}
+ @click=${() => {
+ this.menuItems.splice(index, 1);
+ this.requestUpdate('menuItems');
+ }}
+ class="remove-button"
+ >Delete</gr-button
+ >
+ </td>
+ </tr>
+ `;
}
- _handleDeleteButton(e: Event) {
- const target = (dom(e) as EventApi).localTarget;
- if (!(target instanceof HTMLElement)) return;
- const index = Number(target.dataset['index']);
- this.splice('menuItems', index, 1);
+ private renderFooterRow() {
+ return html`
+ <tr>
+ <th>
+ <iron-input
+ .bindValue=${this.newName}
+ @bind-value-changed=${(e: BindValueChangeEvent) => {
+ this.newName = e.detail.value ?? '';
+ }}
+ >
+ <input
+ is="iron-input"
+ placeholder="New Title"
+ @keydown=${this.handleInputKeydown}
+ />
+ </iron-input>
+ </th>
+ <th>
+ <iron-input
+ .bindValue=${this.newUrl}
+ @bind-value-changed=${(e: BindValueChangeEvent) => {
+ this.newUrl = e.detail.value ?? '';
+ }}
+ >
+ <input
+ class="newUrlInput"
+ placeholder="New URL"
+ @keydown=${this.handleInputKeydown}
+ />
+ </iron-input>
+ </th>
+ <th></th>
+ <th></th>
+ <th>
+ <gr-button
+ id="add"
+ link
+ ?disabled=${this.newName.length === 0 || this.newUrl.length === 0}
+ @click=${this.handleAddButton}
+ >Add</gr-button
+ >
+ </th>
+ </tr>
+ `;
}
- _handleAddButton() {
- if (this._computeAddDisabled(this._newName, this._newUrl)) {
- return;
- }
+ private handleSave() {
+ this.userModel.updatePreferences({
+ ...this.originalPrefs,
+ my: this.menuItems,
+ });
+ }
- this.splice('menuItems', this.menuItems.length, 0, {
- name: this._newName,
- url: this._newUrl,
+ private handleReset() {
+ this.menuItems = [...this.originalPrefs.my];
+ }
+
+ private swapItems(i: number, j: number) {
+ const max = this.menuItems.length - 1;
+ if (i < 0 || j < 0) return;
+ if (i > max || j > max) return;
+ const x = this.menuItems[i];
+ this.menuItems[i] = this.menuItems[j];
+ this.menuItems[j] = x;
+ this.requestUpdate('menuItems');
+ }
+
+ // visible for testing
+ handleAddButton() {
+ if (this.newName.length === 0 || this.newUrl.length === 0) return;
+
+ this.menuItems.push({
+ name: this.newName,
+ url: this.newUrl,
target: '_blank',
});
-
- this._newName = '';
- this._newUrl = '';
+ this.newName = '';
+ this.newUrl = '';
+ this.requestUpdate('menuItems');
}
- _computeAddDisabled(newName?: string, newUrl?: string) {
- return !newName?.length || !newUrl?.length;
- }
-
- _handleInputKeydown(e: KeyboardEvent) {
+ private handleInputKeydown(e: KeyboardEvent) {
if (e.keyCode === 13) {
e.stopPropagation();
- this._handleAddButton();
+ this.handleAddButton();
}
}
}
diff --git a/polygerrit-ui/app/elements/settings/gr-menu-editor/gr-menu-editor_html.ts b/polygerrit-ui/app/elements/settings/gr-menu-editor/gr-menu-editor_html.ts
deleted file mode 100644
index e4d66e2..0000000
--- a/polygerrit-ui/app/elements/settings/gr-menu-editor/gr-menu-editor_html.ts
+++ /dev/null
@@ -1,131 +0,0 @@
-/**
- * @license
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-import {html} from '@polymer/polymer/lib/utils/html-tag';
-
-export const htmlTemplate = html`
- <style include="shared-styles">
- .buttonColumn {
- width: 2em;
- }
- .moveUpButton,
- .moveDownButton {
- width: 100%;
- }
- tbody tr:first-of-type td .moveUpButton,
- tbody tr:last-of-type td .moveDownButton {
- display: none;
- }
- td.urlCell {
- word-break: break-word;
- }
- .newUrlInput {
- min-width: 23em;
- }
- </style>
- <style include="gr-form-styles">
- /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
- </style>
- <div class="gr-form-styles">
- <table>
- <thead>
- <tr>
- <th class="nameHeader">Name</th>
- <th class="url-header">URL</th>
- </tr>
- </thead>
- <tbody>
- <template is="dom-repeat" items="[[menuItems]]">
- <tr>
- <td>[[item.name]]</td>
- <td class="urlCell">[[item.url]]</td>
- <td class="buttonColumn">
- <gr-button
- link=""
- data-index$="[[index]]"
- on-click="_handleMoveUpButton"
- class="moveUpButton"
- >↑</gr-button
- >
- </td>
- <td class="buttonColumn">
- <gr-button
- link=""
- data-index$="[[index]]"
- on-click="_handleMoveDownButton"
- class="moveDownButton"
- >↓</gr-button
- >
- </td>
- <td>
- <gr-button
- link=""
- data-index$="[[index]]"
- on-click="_handleDeleteButton"
- class="remove-button"
- >Delete</gr-button
- >
- </td>
- </tr>
- </template>
- </tbody>
- <tfoot>
- <tr>
- <th>
- <iron-input
- placeholder="New Title"
- on-keydown="_handleInputKeydown"
- bind-value="{{_newName}}"
- >
- <input
- is="iron-input"
- placeholder="New Title"
- on-keydown="_handleInputKeydown"
- bind-value="{{_newName}}"
- />
- </iron-input>
- </th>
- <th>
- <iron-input
- class="newUrlInput"
- placeholder="New URL"
- on-keydown="_handleInputKeydown"
- bind-value="{{_newUrl}}"
- >
- <input
- class="newUrlInput"
- is="iron-input"
- placeholder="New URL"
- on-keydown="_handleInputKeydown"
- bind-value="{{_newUrl}}"
- />
- </iron-input>
- </th>
- <th></th>
- <th></th>
- <th>
- <gr-button
- link=""
- disabled$="[[_computeAddDisabled(_newName, _newUrl)]]"
- on-click="_handleAddButton"
- >Add</gr-button
- >
- </th>
- </tr>
- </tfoot>
- </table>
- </div>
-`;
diff --git a/polygerrit-ui/app/elements/settings/gr-menu-editor/gr-menu-editor_test.ts b/polygerrit-ui/app/elements/settings/gr-menu-editor/gr-menu-editor_test.ts
index 9785ccb..c6130df 100644
--- a/polygerrit-ui/app/elements/settings/gr-menu-editor/gr-menu-editor_test.ts
+++ b/polygerrit-ui/app/elements/settings/gr-menu-editor/gr-menu-editor_test.ts
@@ -1,29 +1,18 @@
/**
* @license
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
+ * Copyright 2016 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
*/
-
import '../../../test/common-test-setup-karma';
import './gr-menu-editor';
import {GrMenuEditor} from './gr-menu-editor';
import * as MockInteractions from '@polymer/iron-test-helpers/mock-interactions';
-import {query, queryAll} from '../../../test/test-utils';
+import {query, queryAndAssert, waitUntil} from '../../../test/test-utils';
import {PaperButtonElement} from '@polymer/paper-button';
import {TopMenuItemInfo} from '../../../types/common';
-
-const basicFixture = fixtureFromElement('gr-menu-editor');
+import {GrButton} from '../../shared/gr-button/gr-button';
+import {fixture, html} from '@open-wc/testing-helpers';
+import {createDefaultPreferences} from '../../../constants/constants';
suite('gr-menu-editor tests', () => {
let element: GrMenuEditor;
@@ -53,52 +42,229 @@
}
setup(async () => {
- element = basicFixture.instantiate();
+ element = await fixture<GrMenuEditor>(
+ html`<gr-menu-editor></gr-menu-editor>`
+ );
menu = [
{url: '/first/url', name: 'first name', target: '_blank'},
{url: '/second/url', name: 'second name', target: '_blank'},
{url: '/third/url', name: 'third name', target: '_blank'},
];
- element.set('menuItems', menu);
- await flush();
+ element.originalPrefs = {...createDefaultPreferences(), my: menu};
+ element.menuItems = [...menu];
+ await element.updateComplete;
});
test('renders', () => {
- const rows = queryAll(query<HTMLElement>(element, 'tbody')!, 'tr');
- let tds;
-
- assert.equal(rows.length, menu.length);
- for (let i = 0; i < menu.length; i++) {
- tds = rows[i].querySelectorAll('td');
- assert.equal(tds[0].textContent, menu[i].name);
- assert.equal(tds[1].textContent, menu[i].url);
- }
-
- assert.isTrue(
- element._computeAddDisabled(element._newName, element._newUrl)
- );
+ expect(element).shadowDom.to.equal(/* HTML */ `
+ <div class="gr-form-styles">
+ <h2 class="heading-2" id="Menu">Menu</h2>
+ <fieldset id="menu">
+ <table>
+ <thead>
+ <tr>
+ <th>Name</th>
+ <th>URL</th>
+ </tr>
+ </thead>
+ <tbody>
+ <tr>
+ <td>first name</td>
+ <td class="urlCell">/first/url</td>
+ <td class="buttonColumn">
+ <gr-button
+ aria-disabled="false"
+ class="moveUpButton"
+ data-index="0"
+ link=""
+ role="button"
+ tabindex="0"
+ >
+ ↑
+ </gr-button>
+ </td>
+ <td class="buttonColumn">
+ <gr-button
+ aria-disabled="false"
+ class="moveDownButton"
+ data-index="0"
+ link=""
+ role="button"
+ tabindex="0"
+ >
+ ↓
+ </gr-button>
+ </td>
+ <td>
+ <gr-button
+ aria-disabled="false"
+ class="remove-button"
+ data-index="0"
+ link=""
+ role="button"
+ tabindex="0"
+ >
+ Delete
+ </gr-button>
+ </td>
+ </tr>
+ <tr>
+ <td>second name</td>
+ <td class="urlCell">/second/url</td>
+ <td class="buttonColumn">
+ <gr-button
+ aria-disabled="false"
+ class="moveUpButton"
+ data-index="1"
+ link=""
+ role="button"
+ tabindex="0"
+ >
+ ↑
+ </gr-button>
+ </td>
+ <td class="buttonColumn">
+ <gr-button
+ aria-disabled="false"
+ class="moveDownButton"
+ data-index="1"
+ link=""
+ role="button"
+ tabindex="0"
+ >
+ ↓
+ </gr-button>
+ </td>
+ <td>
+ <gr-button
+ aria-disabled="false"
+ class="remove-button"
+ data-index="1"
+ link=""
+ role="button"
+ tabindex="0"
+ >
+ Delete
+ </gr-button>
+ </td>
+ </tr>
+ <tr>
+ <td>third name</td>
+ <td class="urlCell">/third/url</td>
+ <td class="buttonColumn">
+ <gr-button
+ aria-disabled="false"
+ class="moveUpButton"
+ data-index="2"
+ link=""
+ role="button"
+ tabindex="0"
+ >
+ ↑
+ </gr-button>
+ </td>
+ <td class="buttonColumn">
+ <gr-button
+ aria-disabled="false"
+ class="moveDownButton"
+ data-index="2"
+ link=""
+ role="button"
+ tabindex="0"
+ >
+ ↓
+ </gr-button>
+ </td>
+ <td>
+ <gr-button
+ aria-disabled="false"
+ class="remove-button"
+ data-index="2"
+ link=""
+ role="button"
+ tabindex="0"
+ >
+ Delete
+ </gr-button>
+ </td>
+ </tr>
+ </tbody>
+ <tfoot>
+ <tr>
+ <th>
+ <iron-input>
+ <input is="iron-input" placeholder="New Title" />
+ </iron-input>
+ </th>
+ <th>
+ <iron-input>
+ <input class="newUrlInput" placeholder="New URL" />
+ </iron-input>
+ </th>
+ <th></th>
+ <th></th>
+ <th>
+ <gr-button
+ aria-disabled="true"
+ disabled=""
+ id="add"
+ link=""
+ role="button"
+ tabindex="-1"
+ >
+ Add
+ </gr-button>
+ </th>
+ </tr>
+ </tfoot>
+ </table>
+ <gr-button
+ aria-disabled="true"
+ disabled=""
+ id="save"
+ role="button"
+ tabindex="-1"
+ >
+ Save changes
+ </gr-button>
+ <gr-button
+ aria-disabled="false"
+ id="reset"
+ link=""
+ role="button"
+ tabindex="0"
+ >
+ Reset
+ </gr-button>
+ </fieldset>
+ </div>
+ `);
});
- test('_computeAddDisabled', () => {
- assert.isTrue(element._computeAddDisabled('', ''));
- assert.isTrue(element._computeAddDisabled('name', ''));
- assert.isTrue(element._computeAddDisabled('', 'url'));
- assert.isFalse(element._computeAddDisabled('name', 'url'));
+ test('add button disabled', async () => {
+ element.newName = 'test-name';
+ await element.updateComplete;
+ let addButton = queryAndAssert<GrButton>(element, 'gr-button#add');
+ assert.isTrue(addButton.hasAttribute('disabled'));
+
+ element.newUrl = 'test-url';
+ await element.updateComplete;
+ addButton = queryAndAssert<GrButton>(element, 'gr-button#add');
+ assert.isFalse(addButton.hasAttribute('disabled'));
});
- test('add a new menu item', () => {
+ test('add a new menu item', async () => {
const newName = 'new name';
const newUrl = 'new url';
-
- element._newName = newName;
- element._newUrl = newUrl;
- assert.isFalse(
- element._computeAddDisabled(element._newName, element._newUrl)
- );
-
const originalMenuLength = element.menuItems.length;
- element._handleAddButton();
+ element.newName = newName;
+ element.newUrl = newUrl;
+ await element.updateComplete;
+
+ const addButton = queryAndAssert<GrButton>(element, 'gr-button#add');
+ assert.isFalse(addButton.hasAttribute('disabled'));
+ addButton.click();
assert.equal(element.menuItems.length, originalMenuLength + 1);
assert.equal(element.menuItems[element.menuItems.length - 1].name, newName);
@@ -117,6 +283,37 @@
assertMenuNamesEqual(element, ['first name', 'third name', 'second name']);
});
+ test('move item down and save', async () => {
+ assertMenuNamesEqual(element, ['first name', 'second name', 'third name']);
+ const saveButton = queryAndAssert<GrButton>(element, 'gr-button#save');
+ assert.isTrue(saveButton.hasAttribute('disabled'));
+
+ move(element, 1, 'Down');
+ await element.updateComplete;
+ assertMenuNamesEqual(element, ['first name', 'third name', 'second name']);
+ assert.isFalse(saveButton.hasAttribute('disabled'));
+
+ saveButton.click();
+ await waitUntil(() => element.originalPrefs.my[1].name === 'third name');
+ await element.updateComplete;
+
+ assertMenuNamesEqual(element, ['first name', 'third name', 'second name']);
+ assert.isTrue(saveButton.hasAttribute('disabled'));
+ });
+
+ test('move item down and reset', async () => {
+ assertMenuNamesEqual(element, ['first name', 'second name', 'third name']);
+
+ move(element, 1, 'Down');
+ assertMenuNamesEqual(element, ['first name', 'third name', 'second name']);
+
+ const resetButton = queryAndAssert<GrButton>(element, 'gr-button#reset');
+ resetButton.click();
+ await element.updateComplete;
+
+ assertMenuNamesEqual(element, ['first name', 'second name', 'third name']);
+ });
+
test('move items up', () => {
assertMenuNamesEqual(element, ['first name', 'second name', 'third name']);
@@ -161,9 +358,9 @@
assertMenuNamesEqual(element, []);
// Add item to empty menu.
- element._newName = 'new name';
- element._newUrl = 'new url';
- element._handleAddButton();
+ element.newName = 'new name';
+ element.newUrl = 'new url';
+ element.handleAddButton();
assertMenuNamesEqual(element, ['new name']);
});
});
diff --git a/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-item.ts b/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-item.ts
index 64409d5..e132128 100644
--- a/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-item.ts
+++ b/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-item.ts
@@ -46,6 +46,6 @@
override render() {
const anchor = this.anchor ?? '';
- return html`<h2 id="${anchor}" class="heading-2">${this.title}</h2>`;
+ return html`<h2 id=${anchor} class="heading-2">${this.title}</h2>`;
}
}
diff --git a/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-menu-item.ts b/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-menu-item.ts
index 9e4ea0a..18c8bea 100644
--- a/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-menu-item.ts
+++ b/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-menu-item.ts
@@ -40,7 +40,7 @@
override render() {
const href = this.href ?? '';
return html` <div class="navStyles">
- <li><a href="${href}">${this.title}</a></li>
+ <li><a href=${href}>${this.title}</a></li>
</div>`;
}
}
diff --git a/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view.ts b/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view.ts
index 5da726c..7a8a771 100644
--- a/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view.ts
+++ b/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view.ts
@@ -25,7 +25,6 @@
import '../../plugins/gr-endpoint-decorator/gr-endpoint-decorator';
import '../gr-change-table-editor/gr-change-table-editor';
import '../../shared/gr-button/gr-button';
-import {GrButton} from '../../shared/gr-button/gr-button';
import '../../shared/gr-diff-preferences/gr-diff-preferences';
import '../../shared/gr-page-nav/gr-page-nav';
import '../../shared/gr-select/gr-select';
@@ -50,11 +49,7 @@
import {GrGroupList} from '../gr-group-list/gr-group-list';
import {GrIdentities} from '../gr-identities/gr-identities';
import {GrDiffPreferences} from '../../shared/gr-diff-preferences/gr-diff-preferences';
-import {
- PreferencesInput,
- ServerInfo,
- TopMenuItemInfo,
-} from '../../../types/common';
+import {PreferencesInput, ServerInfo} from '../../../types/common';
import {GrSshEditor} from '../gr-ssh-editor/gr-ssh-editor';
import {GrGpgEditor} from '../gr-gpg-editor/gr-gpg-editor';
import {GrEmailEditor} from '../gr-email-editor/gr-email-editor';
@@ -103,8 +98,6 @@
LocalPrefsToPrefs,
}
-type LocalMenuItemInfo = Omit<TopMenuItemInfo, 'id'>;
-
export interface GrSettingsView {
$: {
accountInfo: GrAccountInfo;
@@ -129,8 +122,6 @@
emailFormatSelect: HTMLInputElement;
defaultBaseForMergesSelect: HTMLInputElement;
diffViewSelect: HTMLInputElement;
- menu: HTMLFieldSetElement;
- resetButton: GrButton;
};
}
@@ -167,9 +158,6 @@
@property({type: Array})
_localChangeTableColumns: string[] = [];
- @property({type: Array})
- _localMenu: LocalMenuItemInfo[] = [];
-
@property({type: Boolean})
_loading = true;
@@ -183,9 +171,6 @@
_diffPrefsChanged = false;
@property({type: Boolean})
- _menuChanged = false;
-
- @property({type: Boolean})
_watchedProjectsChanged = false;
@property({type: Boolean})
@@ -247,7 +232,6 @@
this.prefs = prefs;
this._showNumber = !!prefs.legacycid_in_change_table;
this._copyPrefs(CopyPrefsDirection.PrefsToLocalPrefs);
- this._localMenu = this._cloneMenu(prefs.my);
this._localChangeTableColumns =
prefs.change_table.length === 0
? columnNames
@@ -316,6 +300,14 @@
super.disconnectedCallback();
}
+ handleUnsavedChangesChanged(e: ValueChangedEvent) {
+ this._keysChanged = !!e.detail.value;
+ }
+
+ _handleGpgEditorHasSavedChanges(e: ValueChangedEvent<boolean>) {
+ this._gpgKeysChanged = e.detail.value;
+ }
+
private readonly handleLocationChange = () => {
// Handle anchor tag after dom attached
const urlHash = window.location.hash;
@@ -351,11 +343,6 @@
}
}
- _cloneMenu(prefs: TopMenuItemInfo[]) {
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
- return prefs.map(({id, ...item}) => item);
- }
-
@observe('_localChangeTableColumns', '_showNumber')
_handleChangeTableChanged() {
if (this._isLoading()) {
@@ -418,14 +405,6 @@
this.set('_localPrefs.signed_off_by', this.$.insertSignedOff.checked);
}
- @observe('_localMenu.splices')
- _handleMenuChanged() {
- if (this._isLoading()) {
- return;
- }
- this._menuChanged = true;
- }
-
_handleSaveAccountInfo() {
this.$.accountInfo.save();
}
@@ -450,21 +429,6 @@
this.$.diffPrefs.save();
}
- _handleSaveMenu() {
- this.set('prefs.my', this._localMenu);
- return this.restApiService.savePreferences(this.prefs).then(() => {
- this._menuChanged = false;
- });
- }
-
- _handleResetMenuButton() {
- return this.restApiService.getDefaultPreferences().then(data => {
- if (data?.my) {
- this._localMenu = this._cloneMenu(data.my);
- }
- });
- }
-
_handleSaveWatchedProjects() {
this.$.watchedProjectsEditor.save();
}
@@ -510,6 +474,22 @@
});
}
+ _handleShowNumberChanged(e: ValueChangedEvent<boolean>) {
+ this._showNumber = e.detail.value;
+ }
+
+ _handleDisplayedColumnsChanged(e: ValueChangedEvent<string[]>) {
+ this._localChangeTableColumns = e.detail.value;
+ }
+
+ _handleHasEmailsChanged(e: ValueChangedEvent<boolean>) {
+ this._emailsChanged = e.detail.value;
+ }
+
+ _handleHasProjectsChanged(e: ValueChangedEvent<boolean>) {
+ this._watchedProjectsChanged = e.detail.value;
+ }
+
_getFilterDocsLink(docsBaseUrl?: string | null) {
let base = docsBaseUrl;
if (!base || !ABSOLUTE_URL_PATTERN.test(base)) {
diff --git a/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view_html.ts b/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view_html.ts
index 394527b..00f85d8 100644
--- a/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view_html.ts
+++ b/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view_html.ts
@@ -361,19 +361,7 @@
>
</fieldset>
<gr-edit-preferences id="editPrefs"></gr-edit-preferences>
- <h2 id="Menu" class$="[[_computeHeaderClass(_menuChanged)]]">Menu</h2>
- <fieldset id="menu">
- <gr-menu-editor menu-items="{{_localMenu}}"></gr-menu-editor>
- <gr-button
- id="saveMenu"
- on-click="_handleSaveMenu"
- disabled="[[!_menuChanged]]"
- >Save changes</gr-button
- >
- <gr-button id="resetButton" link="" on-click="_handleResetMenuButton"
- >Reset</gr-button
- >
- </fieldset>
+ <gr-menu-editor></gr-menu-editor>
<h2
id="ChangeTableColumns"
class$="[[_computeHeaderClass(_changeTableChanged)]]"
@@ -382,9 +370,11 @@
</h2>
<fieldset id="changeTableColumns">
<gr-change-table-editor
- show-number="{{_showNumber}}"
+ show-number="[[_showNumber]]"
+ on-show-number-changed="_handleShowNumberChanged"
server-config="[[_serverConfig]]"
- displayed-columns="{{_localChangeTableColumns}}"
+ displayed-columns="[[_localChangeTableColumns]]"
+ on-displayed-columns-changed="_handleDisplayedColumnsChanged"
>
</gr-change-table-editor>
<gr-button
@@ -402,7 +392,8 @@
</h2>
<fieldset id="watchedProjects">
<gr-watched-projects-editor
- has-unsaved-changes="{{_watchedProjectsChanged}}"
+ has-unsaved-changes="[[_watchedProjectsChanged]]"
+ on-has-unsaved-changes-changed="_handleHasProjectsChanged"
id="watchedProjectsEditor"
></gr-watched-projects-editor>
<gr-button
@@ -418,7 +409,8 @@
<fieldset id="email">
<gr-email-editor
id="emailEditor"
- has-unsaved-changes="{{_emailsChanged}}"
+ has-unsaved-changes="[[_emailsChanged]]"
+ on-has-unsaved-changes-changed="_handleHasEmailsChanged"
></gr-email-editor>
<gr-button on-click="_handleSaveEmails" disabled$="[[!_emailsChanged]]"
>Save changes</gr-button
@@ -474,7 +466,7 @@
</h2>
<gr-ssh-editor
id="sshEditor"
- has-unsaved-changes="{{_keysChanged}}"
+ has-unsaved-changes-changed="handleUnsavedChangesChanged"
></gr-ssh-editor>
</div>
<div hidden$="[[!_serverConfig.receive.enable_signed_push]]">
@@ -483,7 +475,8 @@
</h2>
<gr-gpg-editor
id="gpgEditor"
- has-unsaved-changes="{{_gpgKeysChanged}}"
+ has-unsaved-changes="[[_gpgKeysChanged]]"
+ on-has-unsaved-changes-changed="_handleGpgEditorHasSavedChanges"
></gr-gpg-editor>
</div>
<h2 id="Groups">Groups</h2>
diff --git a/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view_test.ts b/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view_test.ts
index 8049124..7f81f42 100644
--- a/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view_test.ts
+++ b/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view_test.ts
@@ -19,7 +19,7 @@
import './gr-settings-view';
import {GrSettingsView} from './gr-settings-view';
import {GerritView} from '../../../services/router/router-model';
-import {queryAll, queryAndAssert, stubRestApi} from '../../../test/test-utils';
+import {queryAndAssert, stubRestApi} from '../../../test/test-utils';
import {
AuthInfo,
AccountDetailInfo,
@@ -259,7 +259,6 @@
);
assert.isFalse(element._prefsChanged);
- assert.isFalse(element._menuChanged);
const publishOnPush = valueOf('Publish comments on push', 'preferences')!
.firstElementChild!;
@@ -267,7 +266,6 @@
MockInteractions.tap(publishOnPush);
assert.isTrue(element._prefsChanged);
- assert.isFalse(element._menuChanged);
stubRestApi('savePreferences').callsFake(prefs => {
assertMenusEqual(prefs.my, preferences.my);
@@ -278,7 +276,6 @@
// Save the change.
await element._handleSavePreferences();
assert.isFalse(element._prefsChanged);
- assert.isFalse(element._menuChanged);
});
test('publish comments on push', async () => {
@@ -288,7 +285,6 @@
)!.firstElementChild!;
MockInteractions.tap(publishCommentsOnPush);
- assert.isFalse(element._menuChanged);
assert.isTrue(element._prefsChanged);
stubRestApi('savePreferences').callsFake(prefs => {
@@ -299,7 +295,6 @@
// Save the change.
await element._handleSavePreferences();
assert.isFalse(element._prefsChanged);
- assert.isFalse(element._menuChanged);
});
test('set new changes work-in-progress', async () => {
@@ -309,7 +304,6 @@
)!.firstElementChild!;
MockInteractions.tap(newChangesWorkInProgress);
- assert.isFalse(element._menuChanged);
assert.isTrue(element._prefsChanged);
stubRestApi('savePreferences').callsFake(prefs => {
@@ -320,40 +314,6 @@
// Save the change.
await element._handleSavePreferences();
assert.isFalse(element._prefsChanged);
- assert.isFalse(element._menuChanged);
- });
-
- test('menu', async () => {
- assert.isFalse(element._menuChanged);
- assert.isFalse(element._prefsChanged);
-
- assertMenusEqual(element._localMenu, preferences.my);
-
- const menu = element.$.menu.firstElementChild!;
- let tableRows = queryAll(menu, 'tbody tr');
- // let tableRows = menu.root.querySelectorAll('tbody tr');
- assert.equal(tableRows.length, preferences.my.length);
-
- // Add a menu item:
- element.splice('_localMenu', 1, 0, {name: 'foo', url: 'bar', target: ''});
- flush();
-
- // tableRows = menu.root.querySelectorAll('tbody tr');
- tableRows = queryAll(menu, 'tbody tr');
- assert.equal(tableRows.length, preferences.my.length + 1);
-
- assert.isTrue(element._menuChanged);
- assert.isFalse(element._prefsChanged);
-
- stubRestApi('savePreferences').callsFake(prefs => {
- assertMenusEqual(prefs.my, element._localMenu);
- return Promise.resolve(createDefaultPreferences());
- });
-
- await element._handleSaveMenu();
- assert.isFalse(element._menuChanged);
- assert.isFalse(element._prefsChanged);
- assertMenusEqual(element.prefs.my, element._localMenu);
});
test('add email validation', () => {
@@ -445,39 +405,6 @@
assert.isTrue(element.prefs.legacycid_in_change_table);
});
- test('reset menu item back to default', async () => {
- const originalMenu = {
- ...createDefaultPreferences(),
- my: [
- {url: '/first/url', name: 'first name', target: '_blank'},
- {url: '/second/url', name: 'second name', target: '_blank'},
- {url: '/third/url', name: 'third name', target: '_blank'},
- ] as TopMenuItemInfo[],
- };
-
- stubRestApi('getDefaultPreferences').returns(Promise.resolve(originalMenu));
-
- const updatedMenu = [
- {url: '/first/url', name: 'first name', target: '_blank'},
- {url: '/second/url', name: 'second name', target: '_blank'},
- {url: '/third/url', name: 'third name', target: '_blank'},
- {url: '/fourth/url', name: 'fourth name', target: '_blank'},
- ];
-
- element.set('_localMenu', updatedMenu);
-
- await element._handleResetMenuButton();
- assertMenusEqual(element._localMenu, originalMenu.my);
- });
-
- test('test that reset button is called', () => {
- const overlayOpen = sinon.stub(element, '_handleResetMenuButton');
-
- MockInteractions.tap(element.$.resetButton);
-
- assert.isTrue(overlayOpen.called);
- });
-
test('_showHttpAuth', () => {
const serverConfig: ServerInfo = {
...createServerInfo(),
diff --git a/polygerrit-ui/app/elements/settings/gr-ssh-editor/gr-ssh-editor.ts b/polygerrit-ui/app/elements/settings/gr-ssh-editor/gr-ssh-editor.ts
index 95d15b8..72c87b2 100644
--- a/polygerrit-ui/app/elements/settings/gr-ssh-editor/gr-ssh-editor.ts
+++ b/polygerrit-ui/app/elements/settings/gr-ssh-editor/gr-ssh-editor.ts
@@ -21,22 +21,19 @@
import '../../shared/gr-overlay/gr-overlay';
import '../../../styles/shared-styles';
import {dom, EventApi} from '@polymer/polymer/lib/legacy/polymer.dom';
-import {PolymerElement} from '@polymer/polymer/polymer-element';
-import {htmlTemplate} from './gr-ssh-editor_html';
-import {property, customElement} from '@polymer/decorators';
import {SshKeyInfo} from '../../../types/common';
import {GrButton} from '../../shared/gr-button/gr-button';
import {IronAutogrowTextareaElement} from '@polymer/iron-autogrow-textarea/iron-autogrow-textarea';
import {GrOverlay} from '../../shared/gr-overlay/gr-overlay';
import {getAppContext} from '../../../services/app-context';
-
-export interface GrSshEditor {
- $: {
- addButton: GrButton;
- newKey: IronAutogrowTextareaElement;
- viewKeyOverlay: GrOverlay;
- };
-}
+import {LitElement, html} from 'lit';
+import {customElement, property, query, state} from 'lit/decorators';
+import {sharedStyles} from '../../../styles/shared-styles';
+import {css} from 'lit';
+import {BindValueChangeEvent} from '../../../types/events';
+import {fire} from '../../../utils/event-util';
+import {PropertyValues} from 'lit';
+import {formStyles} from '../../../styles/gr-form-styles';
declare global {
interface HTMLElementTagNameMap {
@@ -44,85 +41,236 @@
}
}
@customElement('gr-ssh-editor')
-export class GrSshEditor extends PolymerElement {
- static get template() {
- return htmlTemplate;
- }
-
- @property({type: Boolean, notify: true})
+export class GrSshEditor extends LitElement {
+ @property({type: Boolean})
hasUnsavedChanges = false;
@property({type: Array})
- _keys: SshKeyInfo[] = [];
+ keys: SshKeyInfo[] = [];
@property({type: Object})
- _keyToView?: SshKeyInfo;
+ keyToView?: SshKeyInfo;
@property({type: String})
- _newKey = '';
+ newKey = '';
@property({type: Array})
- _keysToRemove: SshKeyInfo[] = [];
+ keysToRemove: SshKeyInfo[] = [];
+
+ @state() prevHasUnsavedChanges = false;
+
+ @query('#addButton') addButton!: GrButton;
+
+ @query('#newKey') newKeyEditor!: IronAutogrowTextareaElement;
+
+ @query('#viewKeyOverlay') viewKeyOverlay!: GrOverlay;
private readonly restApiService = getAppContext().restApiService;
+ static override get styles() {
+ return [
+ sharedStyles,
+ formStyles,
+ css`
+ .statusHeader {
+ width: 4em;
+ }
+ .keyHeader {
+ width: 7.5em;
+ }
+ #viewKeyOverlay {
+ padding: var(--spacing-xxl);
+ width: 50em;
+ }
+ .publicKey {
+ font-family: var(--monospace-font-family);
+ font-size: var(--font-size-mono);
+ line-height: var(--line-height-mono);
+ overflow-x: scroll;
+ overflow-wrap: break-word;
+ width: 30em;
+ }
+ .closeButton {
+ bottom: 2em;
+ position: absolute;
+ right: 2em;
+ }
+ #existing {
+ margin-bottom: var(--spacing-l);
+ }
+ #existing .commentColumn {
+ min-width: 27em;
+ width: auto;
+ }
+ `,
+ ];
+ }
+
+ override updated(changedProperties: PropertyValues) {
+ if (changedProperties.has('hasUnsavedChanges')) {
+ if (this.prevHasUnsavedChanges === this.hasUnsavedChanges) return;
+ this.prevHasUnsavedChanges = this.hasUnsavedChanges;
+ fire(this, 'has-unsaved-changes-changed', {
+ value: this.hasUnsavedChanges,
+ });
+ }
+ }
+
+ override render() {
+ return html`
+ <div class="gr-form-styles">
+ <fieldset id="existing">
+ <table>
+ <thead>
+ <tr>
+ <th class="commentColumn">Comment</th>
+ <th class="statusHeader">Status</th>
+ <th class="keyHeader">Public key</th>
+ <th></th>
+ <th></th>
+ </tr>
+ </thead>
+ <tbody>
+ ${this.keys.map((key, index) => this.renderKey(key, index))}
+ </tbody>
+ </table>
+ <gr-overlay id="viewKeyOverlay" with-backdrop="">
+ <fieldset>
+ <section>
+ <span class="title">Algorithm</span>
+ <span class="value">${this.keyToView?.algorithm}</span>
+ </section>
+ <section>
+ <span class="title">Public key</span>
+ <span class="value publicKey"
+ >${this.keyToView?.encoded_key}</span
+ >
+ </section>
+ <section>
+ <span class="title">Comment</span>
+ <span class="value">${this.keyToView?.comment}</span>
+ </section>
+ </fieldset>
+ <gr-button
+ class="closeButton"
+ @click=${() => this.viewKeyOverlay.close()}
+ >Close</gr-button
+ >
+ </gr-overlay>
+ <gr-button
+ @click=${() => this.save()}
+ ?disabled=${!this.hasUnsavedChanges}
+ >Save changes</gr-button
+ >
+ </fieldset>
+ <fieldset>
+ <section>
+ <span class="title">New SSH key</span>
+ <span class="value">
+ <iron-autogrow-textarea
+ id="newKey"
+ autocomplete="on"
+ .bindValue=${this.newKey}
+ placeholder="New SSH Key"
+ @bind-value-changed=${(e: BindValueChangeEvent) => {
+ this.newKey = e.detail.value;
+ }}
+ ></iron-autogrow-textarea>
+ </span>
+ </section>
+ <gr-button
+ id="addButton"
+ link=""
+ ?disabled=${!this.newKey.length}
+ @click=${() => this.handleAddKey()}
+ >Add new SSH key</gr-button
+ >
+ </fieldset>
+ </div>
+ `;
+ }
+
+ private renderKey(key: SshKeyInfo, index: number) {
+ return html` <tr>
+ <td class="commentColumn">${key.comment}</td>
+ <td>${key.valid ? 'Valid' : 'Invalid'}</td>
+ <td>
+ <gr-button
+ link=""
+ @click=${(e: Event) => this.showKey(e)}
+ data-index=${index}
+ >Click to View</gr-button
+ >
+ </td>
+ <td>
+ <gr-copy-clipboard
+ hasTooltip=""
+ .buttonTitle=${'Copy SSH public key to clipboard'}
+ hideInput=""
+ .text=${key.ssh_public_key}
+ >
+ </gr-copy-clipboard>
+ </td>
+ <td>
+ <gr-button
+ link=""
+ data-index=${index}
+ @click=${(e: Event) => this.handleDeleteKey(e)}
+ >Delete</gr-button
+ >
+ </td>
+ </tr>`;
+ }
+
loadData() {
return this.restApiService.getAccountSSHKeys().then(keys => {
if (!keys) return;
- this._keys = keys;
+ this.keys = keys;
});
}
+ // private but used in tests
save() {
- const promises = this._keysToRemove.map(key =>
+ const promises = this.keysToRemove.map(key =>
this.restApiService.deleteAccountSSHKey(`${key.seq}`)
);
return Promise.all(promises).then(() => {
- this._keysToRemove = [];
+ this.keysToRemove = [];
this.hasUnsavedChanges = false;
});
}
- _getStatusLabel(isValid: boolean) {
- return isValid ? 'Valid' : 'Invalid';
- }
-
- _showKey(e: Event) {
+ private showKey(e: Event) {
const el = (dom(e) as EventApi).localTarget as GrButton;
const index = Number(el.getAttribute('data-index')!);
- this._keyToView = this._keys[index];
- this.$.viewKeyOverlay.open();
+ this.keyToView = this.keys[index];
+ this.viewKeyOverlay.open();
}
- _closeOverlay() {
- this.$.viewKeyOverlay.close();
- }
-
- _handleDeleteKey(e: Event) {
+ private handleDeleteKey(e: Event) {
const el = (dom(e) as EventApi).localTarget as GrButton;
const index = Number(el.getAttribute('data-index')!);
- this.push('_keysToRemove', this._keys[index]);
- this.splice('_keys', index, 1);
+ this.keysToRemove.push(this.keys[index]);
+ this.keys.splice(index, 1);
+ this.requestUpdate();
this.hasUnsavedChanges = true;
}
- _handleAddKey() {
- this.$.addButton.disabled = true;
- this.$.newKey.disabled = true;
+ // private but used in tests
+ handleAddKey() {
+ this.addButton.disabled = true;
+ this.newKeyEditor.disabled = true;
return this.restApiService
- .addAccountSSHKey(this._newKey.trim())
+ .addAccountSSHKey(this.newKey.trim())
.then(key => {
- this.$.newKey.disabled = false;
- this._newKey = '';
- this.push('_keys', key);
+ this.newKeyEditor.disabled = false;
+ this.newKey = '';
+ this.keys.push(key);
+ this.requestUpdate();
})
.catch(() => {
- this.$.addButton.disabled = false;
- this.$.newKey.disabled = false;
+ this.addButton.disabled = false;
+ this.newKeyEditor.disabled = false;
});
}
-
- _computeAddButtonDisabled(newKey: string) {
- return !newKey.length;
- }
}
diff --git a/polygerrit-ui/app/elements/settings/gr-ssh-editor/gr-ssh-editor_html.ts b/polygerrit-ui/app/elements/settings/gr-ssh-editor/gr-ssh-editor_html.ts
deleted file mode 100644
index e853b58..0000000
--- a/polygerrit-ui/app/elements/settings/gr-ssh-editor/gr-ssh-editor_html.ts
+++ /dev/null
@@ -1,142 +0,0 @@
-/**
- * @license
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-import {html} from '@polymer/polymer/lib/utils/html-tag';
-
-export const htmlTemplate = html`
- <style include="shared-styles">
- /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
- </style>
- <style include="gr-form-styles">
- .statusHeader {
- width: 4em;
- }
- .keyHeader {
- width: 7.5em;
- }
- #viewKeyOverlay {
- padding: var(--spacing-xxl);
- width: 50em;
- }
- .publicKey {
- font-family: var(--monospace-font-family);
- font-size: var(--font-size-mono);
- line-height: var(--line-height-mono);
- overflow-x: scroll;
- overflow-wrap: break-word;
- width: 30em;
- }
- .closeButton {
- bottom: 2em;
- position: absolute;
- right: 2em;
- }
- #existing {
- margin-bottom: var(--spacing-l);
- }
- #existing .commentColumn {
- min-width: 27em;
- width: auto;
- }
- </style>
- <div class="gr-form-styles">
- <fieldset id="existing">
- <table>
- <thead>
- <tr>
- <th class="commentColumn">Comment</th>
- <th class="statusHeader">Status</th>
- <th class="keyHeader">Public key</th>
- <th></th>
- <th></th>
- </tr>
- </thead>
- <tbody>
- <template is="dom-repeat" items="[[_keys]]" as="key">
- <tr>
- <td class="commentColumn">[[key.comment]]</td>
- <td>[[_getStatusLabel(key.valid)]]</td>
- <td>
- <gr-button link="" on-click="_showKey" data-index$="[[index]]"
- >Click to View</gr-button
- >
- </td>
- <td>
- <gr-copy-clipboard
- hasTooltip=""
- buttonTitle="Copy SSH public key to clipboard"
- hideInput=""
- text="[[key.ssh_public_key]]"
- >
- </gr-copy-clipboard>
- </td>
- <td>
- <gr-button
- link=""
- data-index$="[[index]]"
- on-click="_handleDeleteKey"
- >Delete</gr-button
- >
- </td>
- </tr>
- </template>
- </tbody>
- </table>
- <gr-overlay id="viewKeyOverlay" with-backdrop="">
- <fieldset>
- <section>
- <span class="title">Algorithm</span>
- <span class="value">[[_keyToView.algorithm]]</span>
- </section>
- <section>
- <span class="title">Public key</span>
- <span class="value publicKey">[[_keyToView.encoded_key]]</span>
- </section>
- <section>
- <span class="title">Comment</span>
- <span class="value">[[_keyToView.comment]]</span>
- </section>
- </fieldset>
- <gr-button class="closeButton" on-click="_closeOverlay"
- >Close</gr-button
- >
- </gr-overlay>
- <gr-button on-click="save" disabled$="[[!hasUnsavedChanges]]"
- >Save changes</gr-button
- >
- </fieldset>
- <fieldset>
- <section>
- <span class="title">New SSH key</span>
- <span class="value">
- <iron-autogrow-textarea
- id="newKey"
- autocomplete="on"
- bind-value="{{_newKey}}"
- placeholder="New SSH Key"
- ></iron-autogrow-textarea>
- </span>
- </section>
- <gr-button
- id="addButton"
- link=""
- disabled$="[[_computeAddButtonDisabled(_newKey)]]"
- on-click="_handleAddKey"
- >Add new SSH key</gr-button
- >
- </fieldset>
- </div>
-`;
diff --git a/polygerrit-ui/app/elements/settings/gr-ssh-editor/gr-ssh-editor_test.ts b/polygerrit-ui/app/elements/settings/gr-ssh-editor/gr-ssh-editor_test.ts
index 6300b76..0f81e9f 100644
--- a/polygerrit-ui/app/elements/settings/gr-ssh-editor/gr-ssh-editor_test.ts
+++ b/polygerrit-ui/app/elements/settings/gr-ssh-editor/gr-ssh-editor_test.ts
@@ -81,7 +81,7 @@
Promise.resolve()
);
- assert.equal(element._keysToRemove.length, 0);
+ assert.equal(element.keysToRemove.length, 0);
assert.isFalse(element.hasUnsavedChanges);
// Get the delete button for the last row.
@@ -92,21 +92,21 @@
MockInteractions.tap(button!);
- assert.equal(element._keys.length, 1);
- assert.equal(element._keysToRemove.length, 1);
- assert.equal(element._keysToRemove[0], lastKey);
+ assert.equal(element.keys.length, 1);
+ assert.equal(element.keysToRemove.length, 1);
+ assert.equal(element.keysToRemove[0], lastKey);
assert.isTrue(element.hasUnsavedChanges);
assert.isFalse(saveStub.called);
await element.save();
assert.isTrue(saveStub.called);
assert.equal(saveStub.lastCall.args[0], `${lastKey.seq}`);
- assert.equal(element._keysToRemove.length, 0);
+ assert.equal(element.keysToRemove.length, 0);
assert.isFalse(element.hasUnsavedChanges);
});
test('show key', () => {
- const openSpy = sinon.spy(element.$.viewKeyOverlay, 'open');
+ const openSpy = sinon.spy(element.viewKeyOverlay, 'open');
// Get the show button for the last row.
const button = query<GrButton>(
@@ -116,7 +116,7 @@
MockInteractions.tap(button!);
- assert.equal(element._keyToView, keys[1]);
+ assert.equal(element.keyToView, keys[1]);
assert.isTrue(openSpy.called);
});
@@ -133,21 +133,23 @@
const addStub = stubRestApi('addAccountSSHKey').resolves(newKeyObject);
- element._newKey = newKeyString;
+ element.newKey = newKeyString;
- assert.isFalse(element.$.addButton.disabled);
- assert.isFalse(element.$.newKey.disabled);
+ await element.updateComplete;
+
+ assert.isFalse(element.addButton.disabled);
+ assert.isFalse(element.newKeyEditor.disabled);
const promise = mockPromise();
- element._handleAddKey().then(() => {
- assert.isTrue(element.$.addButton.disabled);
- assert.isFalse(element.$.newKey.disabled);
- assert.equal(element._keys.length, 3);
+ element.handleAddKey().then(() => {
+ assert.isTrue(element.addButton.disabled);
+ assert.isFalse(element.newKeyEditor.disabled);
+ assert.equal(element.keys.length, 3);
promise.resolve();
});
- assert.isTrue(element.$.addButton.disabled);
- assert.isTrue(element.$.newKey.disabled);
+ assert.isTrue(element.addButton.disabled);
+ assert.isTrue(element.newKeyEditor.disabled);
assert.isTrue(addStub.called);
assert.equal(addStub.lastCall.args[0], newKeyString);
@@ -159,21 +161,23 @@
const addStub = stubRestApi('addAccountSSHKey').rejects(new Error('error'));
- element._newKey = newKeyString;
+ element.newKey = newKeyString;
- assert.isFalse(element.$.addButton.disabled);
- assert.isFalse(element.$.newKey.disabled);
+ await element.updateComplete;
+
+ assert.isFalse(element.addButton.disabled);
+ assert.isFalse(element.newKeyEditor.disabled);
const promise = mockPromise();
- element._handleAddKey().then(() => {
- assert.isFalse(element.$.addButton.disabled);
- assert.isFalse(element.$.newKey.disabled);
- assert.equal(element._keys.length, 2);
+ element.handleAddKey().then(() => {
+ assert.isFalse(element.addButton.disabled);
+ assert.isFalse(element.newKeyEditor.disabled);
+ assert.equal(element.keys.length, 2);
promise.resolve();
});
- assert.isTrue(element.$.addButton.disabled);
- assert.isTrue(element.$.newKey.disabled);
+ assert.isTrue(element.addButton.disabled);
+ assert.isTrue(element.newKeyEditor.disabled);
assert.isTrue(addStub.called);
assert.equal(addStub.lastCall.args[0], newKeyString);
diff --git a/polygerrit-ui/app/elements/settings/gr-watched-projects-editor/gr-watched-projects-editor.ts b/polygerrit-ui/app/elements/settings/gr-watched-projects-editor/gr-watched-projects-editor.ts
index 32ca2c5..2eb06a5 100644
--- a/polygerrit-ui/app/elements/settings/gr-watched-projects-editor/gr-watched-projects-editor.ts
+++ b/polygerrit-ui/app/elements/settings/gr-watched-projects-editor/gr-watched-projects-editor.ts
@@ -17,23 +17,28 @@
import '@polymer/iron-input/iron-input';
import '../../shared/gr-autocomplete/gr-autocomplete';
import '../../shared/gr-button/gr-button';
-import '../../../styles/gr-form-styles';
-import '../../../styles/shared-styles';
-import {dom, EventApi} from '@polymer/polymer/lib/legacy/polymer.dom';
-import {PolymerElement} from '@polymer/polymer/polymer-element';
-import {htmlTemplate} from './gr-watched-projects-editor_html';
-import {customElement, property} from '@polymer/decorators';
+import {customElement, property, query} from 'lit/decorators';
import {
AutocompleteQuery,
GrAutocomplete,
AutocompleteSuggestion,
} from '../../shared/gr-autocomplete/gr-autocomplete';
-import {hasOwnProperty} from '../../../utils/common-util';
-import {ProjectWatchInfo} from '../../../types/common';
+import {assertIsDefined} from '../../../utils/common-util';
+import {ProjectWatchInfo, RepoName} from '../../../types/common';
import {getAppContext} from '../../../services/app-context';
-import {IronInputElement} from '@polymer/iron-input';
+import {css, html, LitElement} from 'lit';
+import {sharedStyles} from '../../../styles/shared-styles';
+import {formStyles} from '../../../styles/gr-form-styles';
+import {when} from 'lit/directives/when';
+import {fire} from '../../../utils/event-util';
-const NOTIFICATION_TYPES = [
+type PropertiesOfType<Source, Type> = {
+ [K in keyof Source]: Source[K] extends Type ? K : never;
+}[keyof Source];
+
+type NotificationKey = PropertiesOfType<Required<ProjectWatchInfo>, boolean>;
+
+const NOTIFICATION_TYPES: Array<{name: string; key: NotificationKey}> = [
{name: 'Changes', key: 'notify_new_changes'},
{name: 'Patches', key: 'notify_new_patch_sets'},
{name: 'Comments', key: 'notify_all_comments'},
@@ -41,50 +46,145 @@
{name: 'Abandons', key: 'notify_abandoned_changes'},
];
-export interface GrWatchedProjectsEditor {
- $: {
- newFilter: HTMLInputElement;
- newFilterInput: IronInputElement;
- newProject: GrAutocomplete;
- };
-}
-
@customElement('gr-watched-projects-editor')
-export class GrWatchedProjectsEditor extends PolymerElement {
- static get template() {
- return htmlTemplate;
- }
+export class GrWatchedProjectsEditor extends LitElement {
+ // Private but used in tests.
+ @query('#newFilter')
+ newFilter?: HTMLInputElement;
- @property({type: Boolean, notify: true})
+ // Private but used in tests.
+ @query('#newProject')
+ newProject?: GrAutocomplete;
+
+ @property({type: Boolean})
hasUnsavedChanges = false;
@property({type: Array})
- _projects?: ProjectWatchInfo[];
+ projects?: ProjectWatchInfo[];
@property({type: Array})
- _projectsToRemove: ProjectWatchInfo[] = [];
+ projectsToRemove: ProjectWatchInfo[] = [];
- @property({type: Object})
- _query: AutocompleteQuery;
+ private readonly query: AutocompleteQuery = input =>
+ this.getProjectSuggestions(input);
private readonly restApiService = getAppContext().restApiService;
- constructor() {
- super();
- this._query = input => this._getProjectSuggestions(input);
+ static override get styles() {
+ return [
+ sharedStyles,
+ formStyles,
+ css`
+ #watchedProjects .notifType {
+ text-align: center;
+ padding: 0 var(--spacing-s);
+ }
+ .notifControl {
+ cursor: pointer;
+ text-align: center;
+ }
+ .notifControl:hover {
+ outline: 1px solid var(--border-color);
+ }
+ .projectFilter {
+ color: var(--deemphasized-text-color);
+ font-style: italic;
+ margin-left: var(--spacing-l);
+ }
+ .newFilterInput {
+ width: 100%;
+ }
+ `,
+ ];
+ }
+
+ override render() {
+ const types = NOTIFICATION_TYPES;
+ return html` <div class="gr-form-styles">
+ <table id="watchedProjects">
+ <thead>
+ <tr>
+ <th>Repo</th>
+ ${types.map(type => html`<th class="notifType">${type.name}</th>`)}
+ <th></th>
+ </tr>
+ </thead>
+ <tbody>
+ ${(this.projects ?? []).map(project => this.renderProject(project))}
+ </tbody>
+ <tfoot>
+ <tr>
+ <th>
+ <gr-autocomplete
+ id="newProject"
+ query=${this.query}
+ threshold="1"
+ allow-non-suggested-values=""
+ tab-complete=""
+ placeholder="Repo"
+ ></gr-autocomplete>
+ </th>
+ <th colspan=${types.length}>
+ <iron-input id="newFilterInput" class="newFilterInput">
+ <input
+ id="newFilter"
+ class="newFilterInput"
+ placeholder="branch:name, or other search expression"
+ />
+ </iron-input>
+ </th>
+ <th>
+ <gr-button link="" @click=${this.handleAddProject}>Add</gr-button>
+ </th>
+ </tr>
+ </tfoot>
+ </table>
+ </div>`;
+ }
+
+ private renderProject(project: ProjectWatchInfo) {
+ const types = NOTIFICATION_TYPES;
+ return html` <tr>
+ <td>
+ ${project.project}
+ ${when(
+ project.filter,
+ () => html`<div class="projectFilter">${project.filter}</div>`
+ )}
+ </td>
+ ${types.map(type => this.renderNotifyControl(project, type.key))}
+ <td>
+ <gr-button
+ link=""
+ @click=${(_e: Event) => this.handleRemoveProject(project)}
+ >Delete</gr-button
+ >
+ </td>
+ </tr>`;
+ }
+
+ private renderNotifyControl(project: ProjectWatchInfo, key: NotificationKey) {
+ return html` <td class="notifControl" @click=${this.handleNotifCellClick}>
+ <input
+ type="checkbox"
+ data-key=${key}
+ @change=${(e: Event) => this.handleCheckboxChange(project, key, e)}
+ ?checked=${!!project[key]}
+ />
+ </td>`;
}
loadData() {
return this.restApiService.getWatchedProjects().then(projs => {
- this._projects = projs;
+ this.projects = projs;
});
}
save() {
let deletePromise: Promise<Response | undefined>;
- if (this._projectsToRemove.length) {
+ if (this.projectsToRemove.length) {
deletePromise = this.restApiService.deleteWatchedProjects(
- this._projectsToRemove
+ this.projectsToRemove
);
} else {
deletePromise = Promise.resolve(undefined);
@@ -92,32 +192,21 @@
return deletePromise
.then(() => {
- if (this._projects) {
- return this.restApiService.saveWatchedProjects(this._projects);
+ if (this.projects) {
+ return this.restApiService.saveWatchedProjects(this.projects);
} else {
return Promise.resolve(undefined);
}
})
.then(projects => {
- this._projects = projects;
- this._projectsToRemove = [];
- this.hasUnsavedChanges = false;
+ this.projects = projects;
+ this.projectsToRemove = [];
+ this.setHasUnsavedChanges(false);
});
}
- _getTypes() {
- return NOTIFICATION_TYPES;
- }
-
- _getTypeCount() {
- return this._getTypes().length;
- }
-
- _computeCheckboxChecked(project: ProjectWatchInfo, key: string) {
- return hasOwnProperty(project, key);
- }
-
- _getProjectSuggestions(input: string) {
+ // private but used in tests.
+ getProjectSuggestions(input: string) {
return this.restApiService.getSuggestedProjects(input).then(response => {
const projects: AutocompleteSuggestion[] = [];
for (const [name, project] of Object.entries(response ?? {})) {
@@ -127,18 +216,18 @@
});
}
- _handleRemoveProject(e: Event) {
- const el = (dom(e) as EventApi).localTarget as HTMLInputElement;
- const dataIndex = el.getAttribute('data-index');
- if (dataIndex === null || !this._projects) return;
- const index = Number(dataIndex);
- const project = this._projects[index];
- this.splice('_projects', index, 1);
- this.push('_projectsToRemove', project);
- this.hasUnsavedChanges = true;
+ private handleRemoveProject(project: ProjectWatchInfo) {
+ if (!this.projects) return;
+ const index = this.projects.indexOf(project);
+ if (index < 0) return;
+ this.projects.splice(index, 1);
+ this.projectsToRemove.push(project);
+ this.requestUpdate();
+ this.setHasUnsavedChanges(true);
}
- _canAddProject(
+ // private but used in tests.
+ canAddProject(
project: string | null,
text: string | null,
filter: string | null
@@ -152,12 +241,12 @@
return true;
}
- if (!this._projects) return true;
+ if (!this.projects) return true;
// Check if the project with filter is already in the list.
- for (let i = 0; i < this._projects.length; i++) {
+ for (let i = 0; i < this.projects.length; i++) {
if (
- this._projects[i].project === project &&
- this.areFiltersEqual(this._projects[i].filter, filter)
+ this.projects[i].project === project &&
+ this.areFiltersEqual(this.projects[i].filter, filter)
) {
return false;
}
@@ -166,14 +255,15 @@
return true;
}
- _getNewProjectIndex(name: string, filter: string | null) {
- if (!this._projects) return;
+ // private but used in tests.
+ getNewProjectIndex(name: string, filter: string | null) {
+ if (!this.projects) return;
let i;
- for (i = 0; i < this._projects.length; i++) {
- const projectFilter = this._projects[i].filter;
+ for (i = 0; i < this.projects.length; i++) {
+ const projectFilter = this.projects[i].filter;
if (
- this._projects[i].project > name ||
- (this._projects[i].project === name &&
+ this.projects[i].project > name ||
+ (this.projects[i].project === name &&
this.isFilterDefined(projectFilter) &&
this.isFilterDefined(filter) &&
projectFilter! > filter!)
@@ -184,43 +274,47 @@
return i;
}
- _handleAddProject() {
- const newProject = this.$.newProject.value;
- const newProjectName = this.$.newProject.text;
- const filter = this.$.newFilter.value || null;
+ // Private but used in tests.
+ handleAddProject() {
+ assertIsDefined(this.newProject, 'newProject');
+ assertIsDefined(this.newFilter, 'newFilter');
+ const newProject = this.newProject.value;
+ const newProjectName = this.newProject.text as RepoName;
+ const filter = this.newFilter.value;
- if (!this._canAddProject(newProject, newProjectName, filter)) {
+ if (!this.canAddProject(newProject, newProjectName, filter)) {
return;
}
- const insertIndex = this._getNewProjectIndex(newProjectName, filter);
+ const insertIndex = this.getNewProjectIndex(newProjectName, filter);
if (insertIndex !== undefined) {
- this.splice('_projects', insertIndex, 0, {
+ this.projects?.splice(insertIndex, 0, {
project: newProjectName,
filter,
_is_local: true,
});
+ this.requestUpdate();
}
- this.$.newProject.clear();
- this.$.newFilter.value = '';
- this.hasUnsavedChanges = true;
+ this.newProject.clear();
+ this.newFilter.value = '';
+ this.setHasUnsavedChanges(true);
}
- _handleCheckboxChange(e: Event) {
- const el = (dom(e) as EventApi).localTarget as HTMLInputElement;
- if (el === null) return;
- const dataIndex = el.getAttribute('data-index');
- const key = el.getAttribute('data-key');
- if (dataIndex === null || key === null) return;
- const index = Number(dataIndex);
+ private handleCheckboxChange(
+ project: ProjectWatchInfo,
+ key: NotificationKey,
+ e: Event
+ ) {
+ const el = e.target as HTMLInputElement;
const checked = el.checked;
- this.set(['_projects', index, key], !!checked);
- this.hasUnsavedChanges = true;
+ project[key] = !!checked;
+ this.requestUpdate();
+ this.setHasUnsavedChanges(true);
}
- _handleNotifCellClick(e: Event) {
+ private handleNotifCellClick(e: Event) {
if (e.target === null) return;
const checkbox = (e.target as HTMLElement).querySelector('input');
if (checkbox) {
@@ -228,6 +322,11 @@
}
}
+ private setHasUnsavedChanges(value: boolean) {
+ this.hasUnsavedChanges = value;
+ fire(this, 'has-unsaved-changes-changed', {value});
+ }
+
isFilterDefined(filter: string | null | undefined) {
return filter !== null && filter !== undefined;
}
diff --git a/polygerrit-ui/app/elements/settings/gr-watched-projects-editor/gr-watched-projects-editor_html.ts b/polygerrit-ui/app/elements/settings/gr-watched-projects-editor/gr-watched-projects-editor_html.ts
deleted file mode 100644
index fb65a03..0000000
--- a/polygerrit-ui/app/elements/settings/gr-watched-projects-editor/gr-watched-projects-editor_html.ts
+++ /dev/null
@@ -1,123 +0,0 @@
-/**
- * @license
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-import {html} from '@polymer/polymer/lib/utils/html-tag';
-
-export const htmlTemplate = html`
- <style include="shared-styles">
- /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
- </style>
- <style include="gr-form-styles">
- #watchedProjects .notifType {
- text-align: center;
- padding: 0 var(--spacing-s);
- }
- .notifControl {
- cursor: pointer;
- text-align: center;
- }
- .notifControl:hover {
- outline: 1px solid var(--border-color);
- }
- .projectFilter {
- color: var(--deemphasized-text-color);
- font-style: italic;
- margin-left: var(--spacing-l);
- }
- .newFilterInput {
- width: 100%;
- }
- </style>
- <div class="gr-form-styles">
- <table id="watchedProjects">
- <thead>
- <tr>
- <th>Repo</th>
- <template is="dom-repeat" items="[[_getTypes()]]">
- <th class="notifType">[[item.name]]</th>
- </template>
- <th></th>
- </tr>
- </thead>
- <tbody>
- <template
- is="dom-repeat"
- items="[[_projects]]"
- as="project"
- index-as="projectIndex"
- >
- <tr>
- <td>
- [[project.project]]
- <template is="dom-if" if="[[project.filter]]">
- <div class="projectFilter">[[project.filter]]</div>
- </template>
- </td>
- <template is="dom-repeat" items="[[_getTypes()]]" as="type">
- <td class="notifControl" on-click="_handleNotifCellClick">
- <input
- type="checkbox"
- data-index$="[[projectIndex]]"
- data-key$="[[type.key]]"
- on-change="_handleCheckboxChange"
- checked$="[[_computeCheckboxChecked(project, type.key)]]"
- />
- </td>
- </template>
- <td>
- <gr-button
- link=""
- data-index$="[[projectIndex]]"
- on-click="_handleRemoveProject"
- >Delete</gr-button
- >
- </td>
- </tr>
- </template>
- </tbody>
- <tfoot>
- <tr>
- <th>
- <gr-autocomplete
- id="newProject"
- query="[[_query]]"
- threshold="1"
- allow-non-suggested-values=""
- tab-complete=""
- placeholder="Repo"
- ></gr-autocomplete>
- </th>
- <th colspan$="[[_getTypeCount()]]">
- <iron-input
- id="newFilterInput"
- class="newFilterInput"
- placeholder="branch:name, or other search expression"
- >
- <input
- id="newFilter"
- class="newFilterInput"
- placeholder="branch:name, or other search expression"
- />
- </iron-input>
- </th>
- <th>
- <gr-button link="" on-click="_handleAddProject">Add</gr-button>
- </th>
- </tr>
- </tfoot>
- </table>
- </div>
-`;
diff --git a/polygerrit-ui/app/elements/settings/gr-watched-projects-editor/gr-watched-projects-editor_test.ts b/polygerrit-ui/app/elements/settings/gr-watched-projects-editor/gr-watched-projects-editor_test.ts
index c0580f6..bc21460 100644
--- a/polygerrit-ui/app/elements/settings/gr-watched-projects-editor/gr-watched-projects-editor_test.ts
+++ b/polygerrit-ui/app/elements/settings/gr-watched-projects-editor/gr-watched-projects-editor_test.ts
@@ -22,6 +22,8 @@
import {ProjectWatchInfo} from '../../../types/common';
import {queryAll, queryAndAssert} from '../../../test/test-utils';
import * as MockInteractions from '@polymer/iron-test-helpers/mock-interactions';
+import {IronInputElement} from '@polymer/iron-input';
+import {assertIsDefined} from '../../../utils/common-util';
const basicFixture = fixtureFromElement('gr-watched-projects-editor');
@@ -70,7 +72,7 @@
element = basicFixture.instantiate();
await element.loadData();
- await flush();
+ await element.updateComplete;
});
test('renders', () => {
@@ -101,69 +103,70 @@
assert.equal(checkedKeys[2], 'notify_all_comments');
});
- test('_getProjectSuggestions empty', async () => {
- const projects = await element._getProjectSuggestions('nonexistent');
+ test('getProjectSuggestions empty', async () => {
+ const projects = await element.getProjectSuggestions('nonexistent');
assert.equal(projects.length, 0);
});
- test('_getProjectSuggestions non-empty', async () => {
- const projects = await element._getProjectSuggestions('the project');
+ test('getProjectSuggestions non-empty', async () => {
+ const projects = await element.getProjectSuggestions('the project');
assert.equal(projects.length, 1);
assert.equal(projects[0].name, 'the project');
});
- test('_getProjectSuggestions non-empty with two letter project', async () => {
- const projects = await element._getProjectSuggestions('th');
+ test('getProjectSuggestions non-empty with two letter project', async () => {
+ const projects = await element.getProjectSuggestions('th');
assert.equal(projects.length, 1);
assert.equal(projects[0].name, 'the project');
});
test('_canAddProject', () => {
- assert.isFalse(element._canAddProject(null, null, null));
+ assert.isFalse(element.canAddProject(null, null, null));
// Can add a project that is not in the list.
- assert.isTrue(element._canAddProject('project d', null, null));
- assert.isTrue(element._canAddProject('project d', null, 'filter 3'));
+ assert.isTrue(element.canAddProject('project d', null, null));
+ assert.isTrue(element.canAddProject('project d', null, 'filter 3'));
// Cannot add a project that is in the list with no filter.
- assert.isFalse(element._canAddProject('project a', null, null));
+ assert.isFalse(element.canAddProject('project a', null, null));
// Can add a project that is in the list if the filter differs.
- assert.isTrue(element._canAddProject('project a', null, 'filter 4'));
+ assert.isTrue(element.canAddProject('project a', null, 'filter 4'));
// Cannot add a project that is in the list with the same filter.
- assert.isFalse(element._canAddProject('project b', null, 'filter 1'));
- assert.isFalse(element._canAddProject('project b', null, 'filter 2'));
+ assert.isFalse(element.canAddProject('project b', null, 'filter 1'));
+ assert.isFalse(element.canAddProject('project b', null, 'filter 2'));
// Can add a project that is in the list using a new filter.
- assert.isTrue(element._canAddProject('project b', null, 'filter 3'));
+ assert.isTrue(element.canAddProject('project b', null, 'filter 3'));
// Can add a project that is not added by the auto complete
- assert.isTrue(element._canAddProject(null, 'test', null));
+ assert.isTrue(element.canAddProject(null, 'test', null));
});
- test('_getNewProjectIndex', () => {
+ test('getNewProjectIndex', () => {
// Projects are sorted in ASCII order.
- assert.equal(element._getNewProjectIndex('project A', 'filter'), 0);
- assert.equal(element._getNewProjectIndex('project a', 'filter'), 1);
+ assert.equal(element.getNewProjectIndex('project A', 'filter'), 0);
+ assert.equal(element.getNewProjectIndex('project a', 'filter'), 1);
// Projects are sorted by filter when the names are equal
- assert.equal(element._getNewProjectIndex('project b', 'filter 0'), 1);
- assert.equal(element._getNewProjectIndex('project b', 'filter 1.5'), 2);
- assert.equal(element._getNewProjectIndex('project b', 'filter 3'), 3);
+ assert.equal(element.getNewProjectIndex('project b', 'filter 0'), 1);
+ assert.equal(element.getNewProjectIndex('project b', 'filter 1.5'), 2);
+ assert.equal(element.getNewProjectIndex('project b', 'filter 3'), 3);
// Projects with filters follow those without
- assert.equal(element._getNewProjectIndex('project c', 'filter'), 4);
+ assert.equal(element.getNewProjectIndex('project c', 'filter'), 4);
});
- test('_handleAddProject', () => {
- element.$.newProject.value = 'project d';
- element.$.newProject.setText('project d');
- element.$.newFilterInput.bindValue = '';
+ test('handleAddProject', () => {
+ assertIsDefined(element.newProject, 'newProject');
+ element.newProject.value = 'project d';
+ element.newProject.setText('project d');
+ queryAndAssert<IronInputElement>(element, '#newFilterInput').bindValue = '';
- element._handleAddProject();
+ element.handleAddProject();
- const projects = element._projects!;
+ const projects = element.projects!;
assert.equal(projects.length, 5);
assert.equal(projects[4].project, 'project d');
assert.isNotOk(projects[4].filter);
@@ -171,18 +174,21 @@
});
test('_handleAddProject with invalid inputs', () => {
- element.$.newProject.value = 'project b';
- element.$.newProject.setText('project b');
- element.$.newFilterInput.bindValue = 'filter 1';
- element.$.newFilter.value = 'filter 1';
+ assertIsDefined(element.newProject, 'newProject');
+ element.newProject.value = 'project b';
+ element.newProject.setText('project b');
+ queryAndAssert<IronInputElement>(element, '#newFilterInput').bindValue =
+ 'filter 1';
+ assertIsDefined(element.newFilter, 'newFilter');
+ element.newFilter.value = 'filter 1';
- element._handleAddProject();
+ element.handleAddProject();
- assert.equal(element._projects!.length, 4);
+ assert.equal(element.projects!.length, 4);
});
- test('_handleRemoveProject', () => {
- assert.deepEqual(element._projectsToRemove, []);
+ test('_handleRemoveProject', async () => {
+ assert.deepEqual(element.projectsToRemove, []);
const button = queryAndAssert(
element,
@@ -190,13 +196,13 @@
);
MockInteractions.tap(button);
- flush();
+ await element.updateComplete;
const rows = queryAndAssert(element, 'table tbody').querySelectorAll('tr');
assert.equal(rows.length, 3);
- assert.equal(element._projectsToRemove.length, 1);
- assert.equal(element._projectsToRemove[0].project, 'project b');
+ assert.equal(element.projectsToRemove.length, 1);
+ assert.equal(element.projectsToRemove[0].project, 'project b');
});
});
diff --git a/polygerrit-ui/app/elements/shared/gr-account-chip/gr-account-chip.ts b/polygerrit-ui/app/elements/shared/gr-account-chip/gr-account-chip.ts
index 28de23c..689a9fb 100644
--- a/polygerrit-ui/app/elements/shared/gr-account-chip/gr-account-chip.ts
+++ b/polygerrit-ui/app/elements/shared/gr-account-chip/gr-account-chip.ts
@@ -190,17 +190,17 @@
`;
return html`${customStyle}
<div
- class="${classMap({
+ class=${classMap({
...this.computeVoteClasses(),
container: true,
transparentBackground: this.transparentBackground,
closeShown: this.removable,
- })}"
+ })}
>
<div>
<gr-account-label
- .account="${this.account}"
- .change="${this.change}"
+ .account=${this.account}
+ .change=${this.change}
?forceAttention=${this.forceAttention}
?highlightAttention=${this.highlightAttention}
.voteableText=${this.voteableText}
@@ -214,10 +214,10 @@
link=""
?hidden=${!this.removable}
aria-label="Remove"
- class="${classMap({
+ class=${classMap({
remove: true,
transparentBackground: this.transparentBackground,
- })}"
+ })}
@click=${this._handleRemoveTap}
>
<iron-icon icon="gr-icons:close"></iron-icon>
diff --git a/polygerrit-ui/app/elements/shared/gr-account-label/gr-account-label.ts b/polygerrit-ui/app/elements/shared/gr-account-label/gr-account-label.ts
index f38f3f13..a505632 100644
--- a/polygerrit-ui/app/elements/shared/gr-account-label/gr-account-label.ts
+++ b/polygerrit-ui/app/elements/shared/gr-account-label/gr-account-label.ts
@@ -230,14 +230,14 @@
false,
this._selfAccount
)}
- title="${this.computeAttentionIconTitle(
+ title=${this.computeAttentionIconTitle(
highlightAttention,
account,
change,
forceAttention,
this.selected,
this._selfAccount
- )}"
+ )}
>
<gr-button
id="attentionButton"
@@ -260,16 +260,13 @@
: ''}
${this.maybeRenderLink(html`
<span
- class="${classMap({
+ class=${classMap({
hovercardTargetWrapper: true,
hasAttention: this.attentionIconShown,
- })}"
+ })}
>
${this.avatarShown
- ? html`<gr-avatar
- .account="${account}"
- imageSize="32"
- ></gr-avatar>`
+ ? html`<gr-avatar .account=${account} imageSize="32"></gr-avatar>`
: ''}
<span
tabindex=${this.hideHovercard ? '-1' : '0'}
@@ -310,7 +307,7 @@
`${this.account._account_id}`
);
if (!url) return span;
- return html`<a class="ownerLink" href="${url}" tabindex="-1">${span}</a>`;
+ return html`<a class="ownerLink" href=${url} tabindex="-1">${span}</a>`;
}
private renderAccountStatusPlugins() {
@@ -324,7 +321,7 @@
>
<gr-endpoint-param
name="accountId"
- .value="${this.account._account_id}"
+ .value=${this.account._account_id}
></gr-endpoint-param>
<span class="rightSidePadding"></span>
</gr-endpoint-decorator>
diff --git a/polygerrit-ui/app/elements/shared/gr-account-list/gr-account-list.ts b/polygerrit-ui/app/elements/shared/gr-account-list/gr-account-list.ts
index 5f0cf7a..1553eef 100644
--- a/polygerrit-ui/app/elements/shared/gr-account-list/gr-account-list.ts
+++ b/polygerrit-ui/app/elements/shared/gr-account-list/gr-account-list.ts
@@ -27,6 +27,8 @@
AccountInfo,
GroupInfo,
EmailAddress,
+ SuggestedReviewerGroupInfo,
+ SuggestedReviewerAccountInfo,
} from '../../../types/common';
import {
ReviewerSuggestionsProvider,
@@ -36,7 +38,7 @@
import {GrAccountChip} from '../gr-account-chip/gr-account-chip';
import {PolymerDeepPropertyChange} from '@polymer/polymer/interfaces';
import {PaperInputElementExt} from '../../../types/types';
-import {fireAlert} from '../../../utils/event-util';
+import {fireAlert, fire} from '../../../utils/event-util';
import {accountOrGroupKey} from '../../../utils/account-util';
const VALID_EMAIL_ALERT = 'Please input a valid email.';
@@ -45,6 +47,9 @@
interface HTMLElementTagNameMap {
'gr-account-list': GrAccountList;
}
+ interface HTMLElementEventMap {
+ 'account-added': CustomEvent<AccountInputDetail>;
+ }
}
export interface GrAccountList {
@@ -53,31 +58,27 @@
};
}
-/**
- * For item added with account info
- */
-export interface AccountObjectInput {
- account: AccountInfo;
-}
-
-/**
- * For item added with group info
- */
-export interface GroupObjectInput {
- group: GroupInfo;
- confirm: boolean;
+export interface AccountInputDetail {
+ account: AccountInput;
}
/** Supported input to be added */
-export type RawAccountInput = string | AccountObjectInput | GroupObjectInput;
+export type RawAccountInput =
+ | string
+ | SuggestedReviewerAccountInfo
+ | SuggestedReviewerGroupInfo;
-// type guards for AccountObjectInput and GroupObjectInput
-function isAccountObject(x: RawAccountInput): x is AccountObjectInput {
- return !!(x as AccountObjectInput).account;
+// type guards for SuggestedReviewerAccountInfo and SuggestedReviewerGroupInfo
+function isAccountObject(
+ x: RawAccountInput
+): x is SuggestedReviewerAccountInfo {
+ return !!(x as SuggestedReviewerAccountInfo).account;
}
-function isGroupObjectInput(x: RawAccountInput): x is GroupObjectInput {
- return !!(x as GroupObjectInput).group;
+function isSuggestedReviewerGroupInfo(
+ x: RawAccountInput
+): x is SuggestedReviewerGroupInfo {
+ return !!(x as SuggestedReviewerGroupInfo).group;
}
// Internal input type with account info
@@ -106,7 +107,7 @@
return !!input._group || !!input.id;
}
-type AccountInput = AccountInfoInput | GroupInfoInput;
+export type AccountInput = AccountInfoInput | GroupInfoInput;
export interface AccountAddition {
account?: AccountInfoInput;
@@ -150,7 +151,7 @@
* Needed for template checking since value is initially set to null.
*/
@property({type: Object, notify: true})
- pendingConfirmation: GroupObjectInput | null = null;
+ pendingConfirmation: SuggestedReviewerGroupInfo | null = null;
@property({type: Boolean})
readonly = false;
@@ -219,18 +220,20 @@
// Append new account or group to the accounts property. We add our own
// internal properties to the account/group here, so we clone the object
// to avoid cluttering up the shared change object.
+ let account;
+ let group;
let itemTypeAdded = 'unknown';
if (isAccountObject(item)) {
- const account = {...item.account, _pendingAdd: true};
+ account = {...item.account, _pendingAdd: true};
this.removeFromPendingRemoval(account);
this.push('accounts', account);
itemTypeAdded = 'account';
- } else if (isGroupObjectInput(item)) {
+ } else if (isSuggestedReviewerGroupInfo(item)) {
if (item.confirm) {
this.pendingConfirmation = item;
return;
}
- const group = {...item.group, _pendingAdd: true, _group: true};
+ group = {...item.group, _pendingAdd: true, _group: true};
this.push('accounts', group);
this.removeFromPendingRemoval(group);
itemTypeAdded = 'group';
@@ -242,13 +245,14 @@
fireAlert(this, VALID_EMAIL_ALERT);
return false;
} else {
- const account = {email: item as EmailAddress, _pendingAdd: true};
+ account = {email: item as EmailAddress, _pendingAdd: true};
this.push('accounts', account);
this.removeFromPendingRemoval(account);
itemTypeAdded = 'email';
}
}
+ fire(this, 'account-added', {account: (account ?? group)! as AccountInput});
this.reporting.reportInteraction(`Add to ${this.id}`, {itemTypeAdded});
this.pendingConfirmation = null;
return true;
diff --git a/polygerrit-ui/app/elements/shared/gr-account-list/gr-account-list_test.ts b/polygerrit-ui/app/elements/shared/gr-account-list/gr-account-list_test.ts
index 23e5a72..d77dec3 100644
--- a/polygerrit-ui/app/elements/shared/gr-account-list/gr-account-list_test.ts
+++ b/polygerrit-ui/app/elements/shared/gr-account-list/gr-account-list_test.ts
@@ -25,8 +25,9 @@
AccountId,
AccountInfo,
EmailAddress,
+ GroupBaseInfo,
GroupId,
- GroupInfo,
+ GroupName,
SuggestedReviewerAccountInfo,
Suggestion,
} from '../../../types/common';
@@ -64,11 +65,12 @@
_account_id: accountId as AccountId,
};
};
- const makeGroup: () => GroupInfo = function () {
+ const makeGroup: () => GroupBaseInfo = function () {
const groupId = `group${++_nextAccountId}`;
return {
id: groupId as GroupId,
_group: true,
+ name: 'abcd' as GroupName,
};
};
@@ -116,7 +118,7 @@
// New accounts are added to end with pendingAdd class.
const newAccount = makeAccount();
- handleAdd({account: newAccount});
+ handleAdd({account: newAccount, count: 1});
flush();
chips = getChips();
assert.equal(chips.length, 3);
@@ -160,7 +162,7 @@
// New groups are added to end with pendingAdd and group classes.
const newGroup = makeGroup();
- handleAdd({group: newGroup, confirm: false});
+ handleAdd({group: newGroup, confirm: false, count: 1});
flush();
chips = getChips();
assert.equal(chips.length, 2);
@@ -301,9 +303,9 @@
assert.equal(element.additions().length, 0);
const newAccount = makeAccount();
- handleAdd({account: newAccount});
+ handleAdd({account: newAccount, count: 1});
const newGroup = makeGroup();
- handleAdd({group: newGroup, confirm: false});
+ handleAdd({group: newGroup, confirm: false, count: 1});
assert.deepEqual(element.additions(), [
{
@@ -317,6 +319,7 @@
id: newGroup.id,
_group: true,
_pendingAdd: true,
+ name: 'abcd' as GroupName,
},
},
]);
@@ -346,6 +349,7 @@
_group: true,
_pendingAdd: true,
confirmed: true,
+ name: 'abcd' as GroupName,
},
},
]);
@@ -362,7 +366,7 @@
test('max-count', () => {
element.maxCount = 1;
const acct = makeAccount();
- handleAdd({account: acct});
+ handleAdd({account: acct, count: 1});
flush();
assert.isTrue(element.$.entry.hasAttribute('hidden'));
});
diff --git a/polygerrit-ui/app/elements/shared/gr-alert/gr-alert.ts b/polygerrit-ui/app/elements/shared/gr-alert/gr-alert.ts
index fa547dc..50ca7a7 100644
--- a/polygerrit-ui/app/elements/shared/gr-alert/gr-alert.ts
+++ b/polygerrit-ui/app/elements/shared/gr-alert/gr-alert.ts
@@ -99,7 +99,7 @@
<gr-button
link=""
class="action"
- ?hidden="${this._hideActionButton}"
+ ?hidden=${this._hideActionButton}
@click=${this._handleActionTap}
>${actionText}
</gr-button>
diff --git a/polygerrit-ui/app/elements/shared/gr-button/gr-button.ts b/polygerrit-ui/app/elements/shared/gr-button/gr-button.ts
index 89bcbc7..7e7dca6 100644
--- a/polygerrit-ui/app/elements/shared/gr-button/gr-button.ts
+++ b/polygerrit-ui/app/elements/shared/gr-button/gr-button.ts
@@ -209,15 +209,15 @@
override render() {
return html`<paper-button
- ?raised="${!this.link && !this.flatten}"
- ?disabled="${this.disabled || this.loading}"
+ ?raised=${!this.link && !this.flatten}
+ ?disabled=${this.disabled || this.loading}
role="button"
tabindex="-1"
part="paper-button"
- class="${classMap({
+ class=${classMap({
voteChip: this.voteChip && !this.isSubmitRequirementsUiEnabled,
newVoteChip: this.voteChip && this.isSubmitRequirementsUiEnabled,
- })}"
+ })}
>
${this.loading ? html`<span class="loadingSpin"></span>` : ''}
<slot></slot>
diff --git a/polygerrit-ui/app/elements/shared/gr-change-status/gr-change-status.ts b/polygerrit-ui/app/elements/shared/gr-change-status/gr-change-status.ts
index d6f6300..dd6077c 100644
--- a/polygerrit-ui/app/elements/shared/gr-change-status/gr-change-status.ts
+++ b/polygerrit-ui/app/elements/shared/gr-change-status/gr-change-status.ts
@@ -165,7 +165,7 @@
}
return html`
- <a class="status-link" href="${this.getStatusLink()}">
+ <a class="status-link" href=${this.getStatusLink()}>
<div class="chip" aria-label="Label: ${this.status}">
${this.computeStatusString()}
${this.showResolveIcon()
diff --git a/polygerrit-ui/app/elements/shared/gr-comment-thread/gr-comment-thread.ts b/polygerrit-ui/app/elements/shared/gr-comment-thread/gr-comment-thread.ts
index 5a92361..471ebd6 100644
--- a/polygerrit-ui/app/elements/shared/gr-comment-thread/gr-comment-thread.ts
+++ b/polygerrit-ui/app/elements/shared/gr-comment-thread/gr-comment-thread.ts
@@ -438,9 +438,7 @@
return html`
${this.renderFileName()}
<div class="pathInfo">
- ${href
- ? html`<a href="${href}">${line}</a>`
- : html`<span>${line}</span>`}
+ ${href ? html`<a href=${href}>${line}</a>` : html`<span>${line}</span>`}
</div>
`;
}
@@ -455,9 +453,9 @@
return html`
<div class="fileName">
${href
- ? html`<a href="${href}">${displayPath}</a>`
+ ? html`<a href=${href}>${displayPath}</a>`
: html`<span>${displayPath}</span>`}
- <gr-copy-clipboard hideInput .text="${displayPath}"></gr-copy-clipboard>
+ <gr-copy-clipboard hideInput .text=${displayPath}></gr-copy-clipboard>
</div>
`;
}
@@ -481,21 +479,21 @@
: !this.unresolved);
return html`
<gr-comment
- .comment="${comment}"
- .comments="${this.thread!.comments}"
- ?initially-collapsed="${initiallyCollapsed}"
- ?robot-button-disabled="${robotButtonDisabled}"
- ?show-patchset="${this.showPatchset}"
- ?show-ported-comment="${this.showPortedComment &&
- comment.id === this.rootId}"
- @create-fix-comment="${this.handleCommentFix}"
- @copy-comment-link="${this.handleCopyLink}"
- @comment-editing-changed="${(e: CustomEvent) => {
+ .comment=${comment}
+ .comments=${this.thread!.comments}
+ ?initially-collapsed=${initiallyCollapsed}
+ ?robot-button-disabled=${robotButtonDisabled}
+ ?show-patchset=${this.showPatchset}
+ ?show-ported-comment=${this.showPortedComment &&
+ comment.id === this.rootId}
+ @create-fix-comment=${this.handleCommentFix}
+ @copy-comment-link=${this.handleCopyLink}
+ @comment-editing-changed=${(e: CustomEvent) => {
if (isDraftOrUnsaved(comment)) this.editing = e.detail;
- }}"
- @comment-unresolved-changed="${(e: CustomEvent) => {
+ }}
+ @comment-unresolved-changed=${(e: CustomEvent) => {
if (isDraftOrUnsaved(comment)) this.unresolved = e.detail;
- }}"
+ }}
></gr-comment>
`;
}
@@ -513,7 +511,7 @@
<div id="actions">
<iron-icon
class="link-icon copy"
- @click="${this.handleCopyLink}"
+ @click=${this.handleCopyLink}
title="Copy link to this comment"
icon="gr-icons:link"
role="button"
@@ -524,16 +522,16 @@
id="replyBtn"
link
class="action reply"
- ?disabled="${this.saving}"
- @click="${() => this.handleCommentReply(false)}"
+ ?disabled=${this.saving}
+ @click=${() => this.handleCommentReply(false)}
>Reply</gr-button
>
<gr-button
id="quoteBtn"
link
class="action quote"
- ?disabled="${this.saving}"
- @click="${() => this.handleCommentReply(true)}"
+ ?disabled=${this.saving}
+ @click=${() => this.handleCommentReply(true)}
>Quote</gr-button
>
${
@@ -543,16 +541,16 @@
id="ackBtn"
link
class="action ack"
- ?disabled="${this.saving}"
- @click="${this.handleCommentAck}"
+ ?disabled=${this.saving}
+ @click=${this.handleCommentAck}
>Ack</gr-button
>
<gr-button
id="doneBtn"
link
class="action done"
- ?disabled="${this.saving}"
- @click="${this.handleCommentDone}"
+ ?disabled=${this.saving}
+ @click=${this.handleCommentDone}
>Done</gr-button
>
`
@@ -572,16 +570,16 @@
<div class="diff-container">
<gr-diff
id="diff"
- .diff="${this.diff}"
- .layers="${this.layers}"
- .path="${this.thread.path}"
- .prefs="${this.prefs}"
- .renderPrefs="${this.renderPrefs}"
- .highlightRange="${this.highlightRange}"
+ .diff=${this.diff}
+ .layers=${this.layers}
+ .path=${this.thread.path}
+ .prefs=${this.prefs}
+ .renderPrefs=${this.renderPrefs}
+ .highlightRange=${this.highlightRange}
>
</gr-diff>
<div class="view-diff-container">
- <a href="${href}">
+ <a href=${href}>
<gr-button link class="view-diff-button">View Diff</gr-button>
</a>
</div>
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 2af218d..c460ad7 100644
--- a/polygerrit-ui/app/elements/shared/gr-comment/gr-comment.ts
+++ b/polygerrit-ui/app/elements/shared/gr-comment/gr-comment.ts
@@ -454,11 +454,11 @@
if (isUnsaved(this.comment) && !this.editing) return;
const classes = {container: true, draft: isDraftOrUnsaved(this.comment)};
return html`
- <div id="container" class="${classMap(classes)}">
+ <div id="container" class=${classMap(classes)}>
<div
class="header"
id="header"
- @click="${() => (this.collapsed = !this.collapsed)}"
+ @click=${() => (this.collapsed = !this.collapsed)}
>
<div class="headerLeft">
${this.renderAuthor()} ${this.renderPortedCommentMessage()}
@@ -486,8 +486,8 @@
const classes = {draft: isDraftOrUnsaved(this.comment)};
return html`
<gr-account-label
- .account="${this.comment?.author ?? this.account}"
- class="${classMap(classes)}"
+ .account=${this.comment?.author ?? this.account}
+ class=${classMap(classes)}
>
</gr-account-label>
`;
@@ -497,8 +497,8 @@
if (!this.showPortedComment) return;
if (!this.comment?.patch_set) return;
return html`
- <a href="${this.getUrlForComment()}">
- <span class="portedMessage" @click="${this.handlePortedMessageClick}">
+ <a href=${this.getUrlForComment()}>
+ <span class="portedMessage" @click=${this.handlePortedMessageClick}>
From patchset ${this.comment?.patch_set}
</span>
</a>
@@ -520,7 +520,7 @@
<gr-tooltip-content
class="draftTooltip"
has-tooltip
- title="${tooltip}"
+ title=${tooltip}
max-width="20em"
show-icon
>
@@ -542,7 +542,7 @@
return html`
<div class="runIdMessage message">
<div class="runIdInformation">
- <a class="robotRunLink" href="${this.comment.url}">
+ <a class="robotRunLink" href=${this.comment.url}>
<span class="robotRun link">Run Details</span>
</a>
</div>
@@ -568,7 +568,7 @@
title="Delete Comment"
link
class="action delete"
- @click="${this.openDeleteCommentOverlay}"
+ @click=${this.openDeleteCommentOverlay}
>
<iron-icon id="icon" icon="gr-icons:delete"></iron-icon>
</gr-button>
@@ -587,10 +587,10 @@
if (!this.comment?.updated || this.collapsed) return;
return html`
<span class="separator"></span>
- <span class="date" tabindex="0" @click="${this.handleAnchorClick}">
+ <span class="date" tabindex="0" @click=${this.handleAnchorClick}>
<gr-date-formatter
withTooltip
- .dateStr="${this.comment.updated}"
+ .dateStr=${this.comment.updated}
></gr-date-formatter>
</span>
`;
@@ -603,14 +603,14 @@
const ariaLabel = this.collapsed ? 'Expand' : 'Collapse';
return html`
<div class="show-hide" tabindex="0">
- <label class="show-hide" aria-label="${ariaLabel}">
+ <label class="show-hide" aria-label=${ariaLabel}>
<input
type="checkbox"
class="show-hide"
- ?checked="${this.collapsed}"
- @change="${() => (this.collapsed = !this.collapsed)}"
+ ?checked=${this.collapsed}
+ @change=${() => (this.collapsed = !this.collapsed)}
/>
- <iron-icon id="icon" icon="${icon}"></iron-icon>
+ <iron-icon id="icon" icon=${icon}></iron-icon>
</label>
</div>
`;
@@ -629,17 +629,17 @@
class="editMessage"
autocomplete="on"
code=""
- ?disabled="${this.saving}"
+ ?disabled=${this.saving}
rows="4"
- text="${this.messageText}"
- @text-changed="${(e: ValueChangedEvent) => {
+ text=${this.messageText}
+ @text-changed=${(e: ValueChangedEvent) => {
// TODO: This is causing a re-render of <gr-comment> on every key
// press. Try to avoid always setting `this.messageText` or at least
// debounce it. Most of the code can just inspect the current value
// of the textare instead of needing a dedicated property.
this.messageText = e.detail.value;
this.autoSaveTrigger$.next();
- }}"
+ }}
></gr-textarea>
`;
}
@@ -651,9 +651,9 @@
gr-diff-selection.-->
<gr-formatted-text
class="message"
- .content="${this.comment?.message}"
- .config="${this.commentLinks}"
- ?noTrailingMargin="${!isDraftOrUnsaved(this.comment)}"
+ .content=${this.comment?.message}
+ .config=${this.commentLinks}
+ ?noTrailingMargin=${!isDraftOrUnsaved(this.comment)}
></gr-formatted-text>
`;
}
@@ -664,7 +664,7 @@
return html`
<iron-icon
class="copy link-icon"
- @click="${this.handleCopyLink}"
+ @click=${this.handleCopyLink}
title="Copy link to this comment"
icon="gr-icons:link"
role="button"
@@ -684,8 +684,8 @@
<input
type="checkbox"
id="resolvedCheckbox"
- ?checked="${!this.unresolved}"
- @change="${this.handleToggleResolved}"
+ ?checked=${!this.unresolved}
+ @change=${this.handleToggleResolved}
/>
Resolved
</label>
@@ -711,9 +711,9 @@
if (this.editing) return;
return html`<gr-button
link
- ?disabled="${this.saving}"
+ ?disabled=${this.saving}
class="action discard"
- @click="${this.discard}"
+ @click=${this.discard}
>Discard</gr-button
>`;
}
@@ -722,9 +722,9 @@
if (this.editing) return;
return html`<gr-button
link
- ?disabled="${this.saving}"
+ ?disabled=${this.saving}
class="action edit"
- @click="${this.edit}"
+ @click=${this.edit}
>Edit</gr-button
>`;
}
@@ -734,9 +734,9 @@
return html`
<gr-button
link
- ?disabled="${this.saving}"
+ ?disabled=${this.saving}
class="action cancel"
- @click="${this.cancel}"
+ @click=${this.cancel}
>Cancel</gr-button
>
`;
@@ -747,9 +747,9 @@
return html`
<gr-button
link
- ?disabled="${this.isSaveDisabled()}"
+ ?disabled=${this.isSaveDisabled()}
class="action save"
- @click="${this.save}"
+ @click=${this.save}
>Save</gr-button
>
`;
@@ -759,7 +759,7 @@
if (!this.account || !isRobot(this.comment)) return;
const endpoint = html`
<gr-endpoint-decorator name="robot-comment-controls">
- <gr-endpoint-param name="comment" .value="${this.comment}">
+ <gr-endpoint-param name="comment" .value=${this.comment}>
</gr-endpoint-param>
</gr-endpoint-decorator>
`;
@@ -778,8 +778,8 @@
link
secondary
class="action show-fix"
- ?disabled="${this.saving}"
- @click="${this.handleShowFix}"
+ ?disabled=${this.saving}
+ @click=${this.handleShowFix}
>
Show Fix
</gr-button>
@@ -791,9 +791,9 @@
return html`
<gr-button
link
- ?disabled="${this.robotButtonDisabled}"
+ ?disabled=${this.robotButtonDisabled}
class="action fix"
- @click="${this.handleFix}"
+ @click=${this.handleFix}
>
Please Fix
</gr-button>
@@ -806,8 +806,8 @@
<gr-overlay id="confirmDeleteOverlay" with-backdrop>
<gr-confirm-delete-comment-dialog
id="confirmDeleteComment"
- @confirm="${this.handleConfirmDeleteComment}"
- @cancel="${this.closeDeleteCommentOverlay}"
+ @confirm=${this.handleConfirmDeleteComment}
+ @cancel=${this.closeDeleteCommentOverlay}
>
</gr-confirm-delete-comment-dialog>
</gr-overlay>
diff --git a/polygerrit-ui/app/elements/shared/gr-confirm-delete-comment-dialog/gr-confirm-delete-comment-dialog.ts b/polygerrit-ui/app/elements/shared/gr-confirm-delete-comment-dialog/gr-confirm-delete-comment-dialog.ts
index 0a9b9c3..f7f960f 100644
--- a/polygerrit-ui/app/elements/shared/gr-confirm-delete-comment-dialog/gr-confirm-delete-comment-dialog.ts
+++ b/polygerrit-ui/app/elements/shared/gr-confirm-delete-comment-dialog/gr-confirm-delete-comment-dialog.ts
@@ -15,32 +15,21 @@
* limitations under the License.
*/
import '../gr-dialog/gr-dialog';
-import '../../../styles/shared-styles';
-import {PolymerElement} from '@polymer/polymer/polymer-element';
-import {htmlTemplate} from './gr-confirm-delete-comment-dialog_html';
-import {property, customElement} from '@polymer/decorators';
+import {css, html, LitElement} from 'lit';
+import {property, query, customElement} from 'lit/decorators';
import {IronAutogrowTextareaElement} from '@polymer/iron-autogrow-textarea';
+import {sharedStyles} from '../../../styles/shared-styles';
+import {assertIsDefined} from '../../../utils/common-util';
+import {BindValueChangeEvent} from '../../../types/events';
declare global {
interface HTMLElementTagNameMap {
'gr-confirm-delete-comment-dialog': GrConfirmDeleteCommentDialog;
}
}
-export interface GrConfirmDeleteCommentDialog {
- $: {
- messageInput: IronAutogrowTextareaElement;
- };
-}
@customElement('gr-confirm-delete-comment-dialog')
-export class GrConfirmDeleteCommentDialog extends PolymerElement {
- static get template() {
- return htmlTemplate;
- }
-
- static get is() {
- return 'gr-confirm-delete-comment-dialog';
- }
+export class GrConfirmDeleteCommentDialog extends LitElement {
/**
* Fired when the confirm button is pressed.
*
@@ -53,14 +42,79 @@
* @event cancel
*/
+ @query('#messageInput')
+ messageInput?: IronAutogrowTextareaElement;
+
@property({type: String})
message = '';
- resetFocus() {
- this.$.messageInput.textarea.focus();
+ static override get styles() {
+ return [
+ sharedStyles,
+ css`
+ :host {
+ display: block;
+ }
+ :host([disabled]) {
+ opacity: 0.5;
+ pointer-events: none;
+ }
+ .main {
+ display: flex;
+ flex-direction: column;
+ width: 100%;
+ }
+ p {
+ margin-bottom: var(--spacing-l);
+ }
+ label {
+ cursor: pointer;
+ display: block;
+ width: 100%;
+ }
+ iron-autogrow-textarea {
+ font-family: var(--monospace-font-family);
+ font-size: var(--font-size-mono);
+ line-height: var(--line-height-mono);
+ width: 73ch; /* Add a char to account for the border. */
+ }
+ `,
+ ];
}
- _handleConfirmTap(e: Event) {
+ override render() {
+ return html` <gr-dialog
+ confirm-label="Delete"
+ @confirm=${this.handleConfirmTap}
+ @cancel=${this.handleCancelTap}
+ >
+ <div class="header" slot="header">Delete Comment</div>
+ <div class="main" slot="main">
+ <p>
+ This is an admin function. Please only use in exceptional
+ circumstances.
+ </p>
+ <label for="messageInput">Enter comment delete reason</label>
+ <iron-autogrow-textarea
+ id="messageInput"
+ class="message"
+ autocomplete="on"
+ placeholder="<Insert reasoning here>"
+ .bindValue=${this.message}
+ @bind-value-changed=${(e: BindValueChangeEvent) => {
+ this.message = e.detail.value;
+ }}
+ ></iron-autogrow-textarea>
+ </div>
+ </gr-dialog>`;
+ }
+
+ resetFocus() {
+ assertIsDefined(this.messageInput, 'messageInput');
+ this.messageInput.textarea.focus();
+ }
+
+ private handleConfirmTap(e: Event) {
e.preventDefault();
e.stopPropagation();
this.dispatchEvent(
@@ -72,7 +126,7 @@
);
}
- _handleCancelTap(e: Event) {
+ private handleCancelTap(e: Event) {
e.preventDefault();
e.stopPropagation();
this.dispatchEvent(
diff --git a/polygerrit-ui/app/elements/shared/gr-confirm-delete-comment-dialog/gr-confirm-delete-comment-dialog_html.ts b/polygerrit-ui/app/elements/shared/gr-confirm-delete-comment-dialog/gr-confirm-delete-comment-dialog_html.ts
deleted file mode 100644
index 6876c1a..0000000
--- a/polygerrit-ui/app/elements/shared/gr-confirm-delete-comment-dialog/gr-confirm-delete-comment-dialog_html.ts
+++ /dev/null
@@ -1,68 +0,0 @@
-/**
- * @license
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-import {html} from '@polymer/polymer/lib/utils/html-tag';
-
-export const htmlTemplate = html`
- <style include="shared-styles">
- :host {
- display: block;
- }
- :host([disabled]) {
- opacity: 0.5;
- pointer-events: none;
- }
- .main {
- display: flex;
- flex-direction: column;
- width: 100%;
- }
- p {
- margin-bottom: var(--spacing-l);
- }
- label {
- cursor: pointer;
- display: block;
- width: 100%;
- }
- iron-autogrow-textarea {
- font-family: var(--monospace-font-family);
- font-size: var(--font-size-mono);
- line-height: var(--line-height-mono);
- width: 73ch; /* Add a char to account for the border. */
- }
- </style>
- <gr-dialog
- confirm-label="Delete"
- on-confirm="_handleConfirmTap"
- on-cancel="_handleCancelTap"
- >
- <div class="header" slot="header">Delete Comment</div>
- <div class="main" slot="main">
- <p>
- This is an admin function. Please only use in exceptional circumstances.
- </p>
- <label for="messageInput">Enter comment delete reason</label>
- <iron-autogrow-textarea
- id="messageInput"
- class="message"
- autocomplete="on"
- placeholder="<Insert reasoning here>"
- bind-value="{{message}}"
- ></iron-autogrow-textarea>
- </div>
- </gr-dialog>
-`;
diff --git a/polygerrit-ui/app/elements/shared/gr-copy-clipboard/gr-copy-clipboard.ts b/polygerrit-ui/app/elements/shared/gr-copy-clipboard/gr-copy-clipboard.ts
index 01422a9..46bb7d1 100644
--- a/polygerrit-ui/app/elements/shared/gr-copy-clipboard/gr-copy-clipboard.ts
+++ b/polygerrit-ui/app/elements/shared/gr-copy-clipboard/gr-copy-clipboard.ts
@@ -97,15 +97,15 @@
<div class="text">
<iron-input
class="copyText"
- @click="${this._handleInputClick}"
+ @click=${this._handleInputClick}
.bindValue=${this.text ?? ''}
>
<input
id="input"
is="iron-input"
- class="${classMap({hideInput: this.hideInput})}"
+ class=${classMap({hideInput: this.hideInput})}
type="text"
- @click="${this._handleInputClick}"
+ @click=${this._handleInputClick}
readonly=""
.value=${this.text ?? ''}
part="text-container-style"
@@ -113,13 +113,13 @@
</iron-input>
<gr-tooltip-content
?has-tooltip=${this.hasTooltip}
- title="${ifDefined(this.buttonTitle)}"
+ title=${ifDefined(this.buttonTitle)}
>
<gr-button
id="copy-clipboard-button"
link=""
class="copyToClipboard"
- @click="${this._copyToClipboard}"
+ @click=${this._copyToClipboard}
aria-label="Click to copy to clipboard"
>
<iron-icon id="icon" icon="gr-icons:content-copy"></iron-icon>
diff --git a/polygerrit-ui/app/elements/shared/gr-dialog/gr-dialog.ts b/polygerrit-ui/app/elements/shared/gr-dialog/gr-dialog.ts
index fcc1673..cf385e8 100644
--- a/polygerrit-ui/app/elements/shared/gr-dialog/gr-dialog.ts
+++ b/polygerrit-ui/app/elements/shared/gr-dialog/gr-dialog.ts
@@ -136,7 +136,7 @@
<footer>
<gr-button
id="cancel"
- class="${this.cancelLabel.length ? '' : 'hidden'}"
+ class=${this.cancelLabel.length ? '' : 'hidden'}
link
?disabled=${this.disableCancel}
@click=${(e: Event) => this.handleCancelTap(e)}
diff --git a/polygerrit-ui/app/elements/shared/gr-download-commands/gr-download-commands.ts b/polygerrit-ui/app/elements/shared/gr-download-commands/gr-download-commands.ts
index 01a72ae..78de898 100644
--- a/polygerrit-ui/app/elements/shared/gr-download-commands/gr-download-commands.ts
+++ b/polygerrit-ui/app/elements/shared/gr-download-commands/gr-download-commands.ts
@@ -145,7 +145,7 @@
return html`
<paper-tabs
id="downloadTabs"
- class="${this.computeShowTabs()}"
+ class=${this.computeShowTabs()}
.selected=${selectedIndex}
@selected-changed=${this.handleTabChange}
>
@@ -160,7 +160,7 @@
private renderCommands() {
return html`
- <div class="commands" ?hidden="${!this.schemes.length}"></div>
+ <div class="commands" ?hidden=${!this.schemes.length}></div>
${this.commands?.map((command, index) =>
this.renderShellCommand(command, index)
)}
@@ -171,7 +171,7 @@
private renderShellCommand(command: Command, index: number) {
return html`
<gr-shell-command
- class="${this.computeClass(command.title)}"
+ class=${this.computeClass(command.title)}
.label=${command.title}
.command=${command.command}
.tooltip=${this.computeTooltip(index)}
diff --git a/polygerrit-ui/app/elements/shared/gr-editable-content/gr-editable-content.ts b/polygerrit-ui/app/elements/shared/gr-editable-content/gr-editable-content.ts
index 6191bd7..9fbcfa8 100644
--- a/polygerrit-ui/app/elements/shared/gr-editable-content/gr-editable-content.ts
+++ b/polygerrit-ui/app/elements/shared/gr-editable-content/gr-editable-content.ts
@@ -20,15 +20,21 @@
import '../../plugins/gr-endpoint-decorator/gr-endpoint-decorator';
import '../../plugins/gr-endpoint-param/gr-endpoint-param';
import '../../plugins/gr-endpoint-slot/gr-endpoint-slot';
-import {PolymerElement} from '@polymer/polymer/polymer-element';
-import {customElement, property} from '@polymer/decorators';
-import {htmlTemplate} from './gr-editable-content_html';
-import {fireAlert, fireEvent} from '../../../utils/event-util';
+import {fire, fireAlert, fireEvent} from '../../../utils/event-util';
import {getAppContext} from '../../../services/app-context';
import {debounce, DelayedTask} from '../../../utils/async-util';
import {queryAndAssert} from '../../../utils/common-util';
import {IronAutogrowTextareaElement} from '@polymer/iron-autogrow-textarea/iron-autogrow-textarea';
import {Interaction} from '../../../constants/reporting';
+import {LitElement, html} from 'lit';
+import {customElement, property, state} from 'lit/decorators';
+import {sharedStyles} from '../../../styles/shared-styles';
+import {css} from 'lit';
+import {PropertyValues} from 'lit';
+import {BindValueChangeEvent, ValueChangedEvent} from '../../../types/events';
+import {nothing} from 'lit';
+import {classMap} from 'lit/directives/class-map';
+import {when} from 'lit/directives/when';
const RESTORED_MESSAGE = 'Content restored from a previous edit.';
const STORAGE_DEBOUNCE_INTERVAL_MS = 400;
@@ -37,14 +43,14 @@
interface HTMLElementTagNameMap {
'gr-editable-content': GrEditableContent;
}
+ interface HTMLElementEventMap {
+ 'content-changed': ValueChangedEvent<string>;
+ 'editing-changed': ValueChangedEvent<boolean>;
+ }
}
@customElement('gr-editable-content')
-export class GrEditableContent extends PolymerElement {
- static get template() {
- return htmlTemplate;
- }
-
+export class GrEditableContent extends LitElement {
/**
* Fired when the save button is pressed.
*
@@ -63,58 +69,35 @@
* @event show-alert
*/
- @property({type: String, notify: true, observer: '_contentChanged'})
+ @property({type: String})
content?: string;
- @property({type: Boolean, reflectToAttribute: true})
+ @property({type: Boolean, reflect: true})
disabled = false;
@property({
type: Boolean,
- observer: '_editingChanged',
- notify: true,
- reflectToAttribute: true,
+ reflect: true,
})
editing = false;
- @property({type: Boolean})
+ @property({type: Boolean, attribute: 'remove-zero-width-space'})
removeZeroWidthSpace?: boolean;
// If no storage key is provided, content is not stored.
- @property({type: String})
+ @property({type: String, attribute: 'storage-key'})
storageKey?: string;
- /** If false, then the "Show more" button was used to expand. */
- @property({type: Boolean})
- _commitCollapsed = true;
-
- @property({type: Boolean})
+ @property({type: Boolean, attribute: 'commit-collapsible'})
commitCollapsible = true;
- @property({
- type: Boolean,
- computed:
- '_computeHideShowAllContainer(hideEditCommitMessage, _hideShowAllButton, editing)',
- })
- _hideShowAllContainer = false;
-
- @property({
- type: Boolean,
- computed: '_computeHideShowAllButton(commitCollapsible, editing)',
- })
- _hideShowAllButton = false;
-
- @property({type: Boolean})
+ @property({type: Boolean, attribute: 'hide-edit-commit-message'})
hideEditCommitMessage?: boolean;
- @property({
- type: Boolean,
- computed: '_computeSaveDisabled(disabled, content, _newContent)',
- })
- _saveDisabled!: boolean;
+ /** If false, then the "Show more" button was used to expand. */
+ @state() commitCollapsed = true;
- @property({type: String, observer: '_newContentChanged'})
- _newContent = '';
+ @state() newContent = '';
private readonly storage = getAppContext().storageService;
@@ -128,12 +111,217 @@
super.disconnectedCallback();
}
- _contentChanged() {
+ override willUpdate(changedProperties: PropertyValues) {
+ if (changedProperties.has('editing')) this.editingChanged();
+ if (changedProperties.has('newContent')) this.newContentChanged();
+ if (changedProperties.has('content')) this.contentChanged();
+ }
+
+ static override get styles() {
+ return [
+ sharedStyles,
+ css`
+ :host {
+ display: block;
+ }
+ :host([disabled]) iron-autogrow-textarea {
+ opacity: 0.5;
+ }
+ .viewer {
+ background-color: var(--view-background-color);
+ border: 1px solid var(--view-background-color);
+ border-radius: var(--border-radius);
+ box-shadow: var(--elevation-level-1);
+ padding: var(--spacing-m);
+ }
+ :host(.collapsed) .viewer,
+ .viewer.collapsed {
+ max-height: var(--collapsed-max-height, 300px);
+ overflow: hidden;
+ }
+ .editor iron-autogrow-textarea,
+ .viewer {
+ min-height: 100px;
+ }
+ .editor iron-autogrow-textarea {
+ background-color: var(--view-background-color);
+ width: 100%;
+ display: block;
+
+ /* You have to also repeat everything from shared-styles here, because
+ you can only *replace* --iron-autogrow-textarea vars as a whole. */
+ --iron-autogrow-textarea: {
+ box-sizing: border-box;
+ padding: var(--spacing-m);
+ overflow-y: hidden;
+ white-space: pre;
+ }
+ }
+ .editButtons {
+ display: flex;
+ justify-content: space-between;
+ }
+ .show-all-container {
+ background-color: var(--view-background-color);
+ display: flex;
+ justify-content: flex-end;
+ border: 1px solid transparent;
+ border-top-color: var(--border-color);
+ border-radius: 0 0 4px 4px;
+ box-shadow: var(--elevation-level-1);
+ /* slightly up to cover rounded corner of the commit msg */
+ margin-top: calc(-1 * var(--spacing-xs));
+ /* To make this bar pop over editor, since editor has relative position.
+ */
+ position: relative;
+ }
+ :host([editing]) .show-all-container {
+ box-shadow: none;
+ border: 1px solid var(--border-color);
+ }
+ .show-all-container .show-all-button {
+ margin-right: auto;
+ }
+ .show-all-container iron-icon {
+ color: inherit;
+ --iron-icon-height: 18px;
+ --iron-icon-width: 18px;
+ }
+ .cancel-button {
+ margin-right: var(--spacing-l);
+ }
+ .save-button {
+ margin-right: var(--spacing-xs);
+ }
+ gr-button {
+ font-family: var(--font-family);
+ line-height: var(--line-height-normal);
+ padding: var(--spacing-xs);
+ }
+ `,
+ ];
+ }
+
+ override render() {
+ return html`
+ <gr-endpoint-decorator name="commit-message">
+ <gr-endpoint-param
+ name="editing"
+ .value=${this.editing}
+ ></gr-endpoint-param>
+ ${this.renderViewer()} ${this.renderEditor()} ${this.renderButtons()}
+ <gr-endpoint-slot name="above-actions"></gr-endpoint-slot>
+ </gr-endpoint-decorator>
+ `;
+ }
+
+ private renderViewer() {
+ if (this.editing) return;
+ return html`
+ <div
+ class=${classMap({
+ viewer: true,
+ collapsed: this.commitCollapsed && this.commitCollapsible,
+ })}
+ >
+ <slot></slot>
+ </div>
+ `;
+ }
+
+ private renderEditor() {
+ if (!this.editing) return;
+ return html`
+ <div class="editor">
+ <div>
+ <iron-autogrow-textarea
+ autocomplete="on"
+ .bindValue=${this.newContent}
+ ?disabled=${this.disabled}
+ @bind-value-changed=${(e: BindValueChangeEvent) => {
+ this.newContent = e.detail.value;
+ }}
+ ></iron-autogrow-textarea>
+ </div>
+ </div>
+ `;
+ }
+
+ private renderButtons() {
+ if (!this.editing && !this.commitCollapsible && this.hideEditCommitMessage)
+ return nothing;
+
+ return html`
+ <div class="show-all-container">
+ ${when(
+ this.commitCollapsible && !this.editing,
+ () => html`
+ <gr-button
+ link
+ class="show-all-button"
+ @click=${this.toggleCommitCollapsed}
+ >
+ ${when(
+ !this.commitCollapsed,
+ () => html`
+ <iron-icon icon="gr-icons:expand-less"></iron-icon>
+ `
+ )}
+ ${when(
+ this.commitCollapsed,
+ () => html`
+ <iron-icon icon="gr-icons:expand-more"></iron-icon>
+ `
+ )}
+ ${this.commitCollapsed ? 'Show all' : 'Show less'}
+ </gr-button>
+ `
+ )}
+ ${when(
+ !this.hideEditCommitMessage,
+ () => html`
+ <gr-button
+ link
+ class="edit-commit-message"
+ title="Edit commit message"
+ @click=${this.handleEditCommitMessage}
+ ><iron-icon icon="gr-icons:edit"></iron-icon> Edit</gr-button
+ >
+ `
+ )}
+ ${when(
+ this.editing,
+ () => html` <div class="editButtons">
+ <gr-button
+ link
+ class="cancel-button"
+ @click=${this.handleCancel}
+ ?disabled=${this.disabled}
+ >Cancel</gr-button
+ >
+ <gr-button
+ class="save-button"
+ primary=""
+ @click=${this.handleSave}
+ ?disabled=${this.computeSaveDisabled()}
+ >Save</gr-button
+ >
+ </div>`
+ )}
+ </div>
+ </div>
+ `;
+ }
+
+ contentChanged() {
/* A changed content means that either a different change has been loaded
* or new content was saved. Either way, let's reset the component.
*/
this.editing = false;
- this._newContent = '';
+ this.newContent = '';
+ fire(this, 'content-changed', {
+ value: this.content ?? '',
+ });
}
focusTextarea() {
@@ -143,15 +331,15 @@
).textarea.focus();
}
- _newContentChanged(newContent: string) {
+ newContentChanged() {
if (!this.storageKey) return;
const storageKey = this.storageKey;
this.storeTask = debounce(
this.storeTask,
() => {
- if (newContent.length) {
- this.storage.setEditableContentItem(storageKey, newContent);
+ if (this.newContent.length) {
+ this.storage.setEditableContentItem(storageKey, this.newContent);
} else {
// This does not really happen, because we don't clear newContent
// after saving (see below). So this only occurs when the user clears
@@ -165,8 +353,8 @@
);
}
- _editingChanged(editing: boolean) {
- // This method is for initializing _newContent when you start editing.
+ editingChanged() {
+ // This method is for initializing newContent when you start editing.
// Restoring content from local storage is not perfect and has
// some issues:
//
@@ -180,7 +368,11 @@
// content from local storage when you enter editing mode for the first
// time. Otherwise it is better to just keep the last editing state from
// the same session.
- if (!editing || this._newContent) return;
+ fire(this, 'editing-changed', {
+ value: this.editing,
+ });
+
+ if (!this.editing || this.newContent) return;
let content;
if (this.storageKey) {
@@ -197,72 +389,49 @@
}
// TODO(wyatta) switch linkify sequence, see issue 5526.
- this._newContent = this.removeZeroWidthSpace
+ this.newContent = this.removeZeroWidthSpace
? content.replace(/^R=\u200B/gm, 'R=')
: content;
}
- _computeSaveDisabled(
- disabled?: boolean,
- content?: string,
- newContent?: string
- ): boolean {
- return disabled || !newContent || content === newContent;
+ computeSaveDisabled(): boolean {
+ return (
+ this.disabled || !this.newContent || this.content === this.newContent
+ );
}
- _handleSave(e: Event) {
+ handleSave(e: Event) {
e.preventDefault();
this.dispatchEvent(
new CustomEvent('editable-content-save', {
- detail: {content: this._newContent},
+ detail: {content: this.newContent},
composed: true,
bubbles: true,
})
);
- // It would be nice, if we would set this._newContent = undefined here,
+ // It would be nice, if we would set this.newContent = undefined here,
// but we can only do that when we are sure that the save operation has
// succeeded.
}
- _handleCancel(e: Event) {
+ handleCancel(e: Event) {
e.preventDefault();
this.editing = false;
fireEvent(this, 'editable-content-cancel');
}
- _computeCollapseText(collapsed: boolean) {
- return collapsed ? 'Show all' : 'Show less';
- }
-
- _toggleCommitCollapsed() {
- this._commitCollapsed = !this._commitCollapsed;
+ toggleCommitCollapsed() {
+ this.commitCollapsed = !this.commitCollapsed;
this.reporting.reportInteraction(Interaction.TOGGLE_SHOW_ALL_BUTTON, {
sectionName: 'Commit message',
- toState: !this._commitCollapsed ? 'Show all' : 'Show less',
+ toState: !this.commitCollapsed ? 'Show all' : 'Show less',
});
- if (this._commitCollapsed) {
+ if (this.commitCollapsed) {
window.scrollTo(0, 0);
}
}
- _computeHideShowAllContainer(
- hideEditCommitMessage?: boolean,
- _hideShowAllButton?: boolean,
- editing?: boolean
- ) {
- if (editing) return false;
- return _hideShowAllButton && hideEditCommitMessage;
- }
-
- _computeHideShowAllButton(commitCollapsible?: boolean, editing?: boolean) {
- return !commitCollapsible || editing;
- }
-
- _computeCommitMessageCollapsed(collapsed?: boolean, collapsible?: boolean) {
- return collapsible && collapsed;
- }
-
- _handleEditCommitMessage() {
+ handleEditCommitMessage() {
this.editing = true;
this.focusTextarea();
}
diff --git a/polygerrit-ui/app/elements/shared/gr-editable-content/gr-editable-content_html.ts b/polygerrit-ui/app/elements/shared/gr-editable-content/gr-editable-content_html.ts
deleted file mode 100644
index 8c40177..0000000
--- a/polygerrit-ui/app/elements/shared/gr-editable-content/gr-editable-content_html.ts
+++ /dev/null
@@ -1,160 +0,0 @@
-/**
- * @license
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-import {html} from '@polymer/polymer/lib/utils/html-tag';
-
-export const htmlTemplate = html`
- <style include="shared-styles">
- :host {
- display: block;
- }
- :host([disabled]) iron-autogrow-textarea {
- opacity: 0.5;
- }
- .viewer {
- background-color: var(--view-background-color);
- border: 1px solid var(--view-background-color);
- border-radius: var(--border-radius);
- box-shadow: var(--elevation-level-1);
- padding: var(--spacing-m);
- }
- :host([collapsed]) .viewer,
- .viewer[collapsed] {
- max-height: var(--collapsed-max-height, 300px);
- overflow: hidden;
- }
- .editor iron-autogrow-textarea,
- .viewer {
- min-height: 100px;
- }
- .editor iron-autogrow-textarea {
- background-color: var(--view-background-color);
- width: 100%;
- display: block;
-
- /* You have to also repeat everything from shared-styles here, because
- you can only *replace* --iron-autogrow-textarea vars as a whole. */
- --iron-autogrow-textarea: {
- box-sizing: border-box;
- padding: var(--spacing-m);
- overflow-y: hidden;
- white-space: pre;
- }
- }
- .editButtons {
- display: flex;
- justify-content: space-between;
- }
- .show-all-container {
- background-color: var(--view-background-color);
- display: flex;
- justify-content: flex-end;
- border: 1px solid transparent;
- border-top-color: var(--border-color);
- border-radius: 0 0 4px 4px;
- box-shadow: var(--elevation-level-1);
- /* slightly up to cover rounded corner of the commit msg */
- margin-top: calc(-1 * var(--spacing-xs));
- /* To make this bar pop over editor, since editor has relative position.
- */
- position: relative;
- }
- :host([editing]) .show-all-container {
- box-shadow: none;
- border: 1px solid var(--border-color);
- }
- .show-all-container .show-all-button {
- margin-right: auto;
- }
- .show-all-container iron-icon {
- color: inherit;
- --iron-icon-height: 18px;
- --iron-icon-width: 18px;
- }
- .cancel-button {
- margin-right: var(--spacing-l);
- }
- .save-button {
- margin-right: var(--spacing-xs);
- }
- gr-button {
- font-family: var(--font-family);
- line-height: var(--line-height-normal);
- padding: var(--spacing-xs);
- }
- </style>
- <gr-endpoint-decorator name="commit-message">
- <gr-endpoint-param name="editing" value="[[editing]]"></gr-endpoint-param>
- <div
- class="viewer"
- hidden$="[[editing]]"
- collapsed$="[[_computeCommitMessageCollapsed(_commitCollapsed, commitCollapsible)]]"
- >
- <slot></slot>
- </div>
- <div class="editor" hidden$="[[!editing]]">
- <div>
- <iron-autogrow-textarea
- autocomplete="on"
- bind-value="{{_newContent}}"
- disabled="[[disabled]]"
- ></iron-autogrow-textarea>
- </div>
- </div>
- <gr-endpoint-slot name="above-actions"></gr-endpoint-slot>
- <div class="show-all-container" hidden$="[[_hideShowAllContainer]]">
- <gr-button
- link=""
- class="show-all-button"
- on-click="_toggleCommitCollapsed"
- hidden$="[[_hideShowAllButton]]"
- ><iron-icon
- icon="gr-icons:expand-more"
- hidden$="[[!_commitCollapsed]]"
- ></iron-icon
- ><iron-icon
- icon="gr-icons:expand-less"
- hidden$="[[_commitCollapsed]]"
- ></iron-icon>
- [[_computeCollapseText(_commitCollapsed)]]
- </gr-button>
- <gr-button
- link=""
- class="edit-commit-message"
- title="Edit commit message"
- on-click="_handleEditCommitMessage"
- hidden$="[[hideEditCommitMessage]]"
- ><iron-icon icon="gr-icons:edit"></iron-icon> Edit</gr-button
- >
- <div class="editButtons" hidden$="[[!editing]]">
- <gr-button
- link=""
- class="cancel-button"
- on-click="_handleCancel"
- disabled="[[disabled]]"
- >Cancel</gr-button
- >
- <gr-button
- class="save-button"
- primary=""
- on-click="_handleSave"
- disabled="[[_saveDisabled]]"
- >Save</gr-button
- >
- </div>
- </div>
- </gr-endpoint-decorator>
-`;
diff --git a/polygerrit-ui/app/elements/shared/gr-editable-content/gr-editable-content_test.ts b/polygerrit-ui/app/elements/shared/gr-editable-content/gr-editable-content_test.ts
index 074678e..9b30591 100644
--- a/polygerrit-ui/app/elements/shared/gr-editable-content/gr-editable-content_test.ts
+++ b/polygerrit-ui/app/elements/shared/gr-editable-content/gr-editable-content_test.ts
@@ -18,33 +18,103 @@
import '../../../test/common-test-setup-karma';
import './gr-editable-content';
import {GrEditableContent} from './gr-editable-content';
-import {queryAndAssert, stubStorage} from '../../../test/test-utils';
+import {query, queryAndAssert, stubStorage} from '../../../test/test-utils';
import * as MockInteractions from '@polymer/iron-test-helpers/mock-interactions';
import {GrButton} from '../gr-button/gr-button';
-
-const basicFixture = fixtureFromElement('gr-editable-content');
+import {fixture, html} from '@open-wc/testing-helpers';
suite('gr-editable-content tests', () => {
let element: GrEditableContent;
- setup(() => {
- element = basicFixture.instantiate();
+ setup(async () => {
+ element = await fixture(html`<gr-editable-content></gr-editable-content>`);
+ await element.updateComplete;
});
- test('save event', () => {
+ test('renders', () => {
+ expect(element).shadowDom.to.equal(/* HTML */ `<gr-endpoint-decorator
+ name="commit-message"
+ >
+ <gr-endpoint-param name="editing"> </gr-endpoint-param>
+ <div class="collapsed viewer">
+ <slot> </slot>
+ </div>
+ <div class="show-all-container">
+ <gr-button
+ aria-disabled="false"
+ class="show-all-button"
+ link=""
+ role="button"
+ tabindex="0"
+ >
+ <iron-icon icon="gr-icons:expand-more"> </iron-icon>
+ Show all
+ </gr-button>
+ <gr-button
+ aria-disabled="false"
+ class="edit-commit-message"
+ link=""
+ role="button"
+ tabindex="0"
+ title="Edit commit message"
+ >
+ <iron-icon icon="gr-icons:edit"> </iron-icon>
+ Edit
+ </gr-button>
+ </div>
+ <gr-endpoint-slot name="above-actions"> </gr-endpoint-slot>
+ </gr-endpoint-decorator> `);
+ });
+
+ test('show-all-container visibility', async () => {
+ element.editing = false;
+ element.commitCollapsible = false;
+ element.hideEditCommitMessage = true;
+ await element.updateComplete;
+ assert.isNotOk(query(element, '.show-all-container'));
+
+ element.hideEditCommitMessage = false;
+ await element.updateComplete;
+ assert.isOk(query(element, '.show-all-container'));
+
+ element.hideEditCommitMessage = true;
+ element.editing = true;
+ await element.updateComplete;
+ assert.isOk(query(element, '.show-all-container'));
+
+ element.editing = false;
+ element.commitCollapsible = true;
+ await element.updateComplete;
+ assert.isOk(query(element, '.show-all-container'));
+ });
+
+ test('save event', async () => {
element.content = '';
- element._newContent = 'foo';
+ // Needed because contentChanged resets newContent
+ // We want contentChanged observer to finish before newContentChanged is
+ // called
+ await element.updateComplete;
+
+ element.newContent = 'foo';
+ element.disabled = false;
+ element.editing = true;
const handler = sinon.spy();
element.addEventListener('editable-content-save', handler);
- MockInteractions.tap(queryAndAssert(element, 'gr-button[primary]'));
+ await element.updateComplete;
+
+ queryAndAssert<GrButton>(element, 'gr-button[primary]').click();
+
+ await element.updateComplete;
assert.isTrue(handler.called);
assert.equal(handler.lastCall.args[0].detail.content, 'foo');
});
- test('cancel event', () => {
+ test('cancel event', async () => {
const handler = sinon.spy();
+ element.editing = true;
+ await element.updateComplete;
element.addEventListener('editable-content-cancel', handler);
MockInteractions.tap(queryAndAssert(element, 'gr-button.cancel-button'));
@@ -52,32 +122,58 @@
assert.isTrue(handler.called);
});
- test('enabling editing keeps old content', () => {
+ test('enabling editing keeps old content', async () => {
element.content = 'current content';
- element._newContent = 'old content';
+
+ // Needed because contentChanged resets newContent
+ // We want contentChanged observer to finish before newContentChanged is
+ // called
+ await element.updateComplete;
+
+ element.newContent = 'old content';
element.editing = true;
- assert.equal(element._newContent, 'old content');
+
+ await element.updateComplete;
+
+ assert.equal(element.newContent, 'old content');
});
test('disabling editing does not update edit field contents', () => {
element.content = 'current content';
element.editing = true;
- element._newContent = 'stale content';
+ element.newContent = 'stale content';
element.editing = false;
- assert.equal(element._newContent, 'stale content');
+ assert.equal(element.newContent, 'stale content');
});
- test('zero width spaces are removed properly', () => {
+ test('zero width spaces are removed properly', async () => {
element.removeZeroWidthSpace = true;
element.content = 'R=\u200Btest@google.com';
+
+ // Needed because contentChanged resets newContent
+ // We want contentChanged observer to finish before editingChanged is
+ // called
+
+ await element.updateComplete;
+
element.editing = true;
- assert.equal(element._newContent, 'R=test@google.com');
+
+ // editingChanged updates newContent so wait for it's observer
+ // to finish
+ await element.updateComplete;
+
+ assert.equal(element.newContent, 'R=test@google.com');
});
suite('editing', () => {
- setup(() => {
+ setup(async () => {
element.content = 'current content';
+ // Needed because contentChanged resets newContent
+ // contentChanged updates newContent as well so wait for that observer
+ // to finish before setting editing=true.
+ await element.updateComplete;
element.editing = true;
+ await element.updateComplete;
});
test('save button is disabled initially', () => {
@@ -86,8 +182,9 @@
);
});
- test('save button is enabled when content changes', () => {
- element._newContent = 'new content';
+ test('save button is enabled when content changes', async () => {
+ element.newContent = 'new content';
+ await element.updateComplete;
assert.isFalse(
queryAndAssert<GrButton>(element, 'gr-button[primary]').disabled
);
@@ -97,49 +194,60 @@
suite('storageKey and related behavior', () => {
let dispatchSpy: sinon.SinonSpy;
- setup(() => {
+ setup(async () => {
element.content = 'current content';
+ await element.updateComplete;
element.storageKey = 'test';
dispatchSpy = sinon.spy(element, 'dispatchEvent');
});
- test('editing toggled to true, has stored data', () => {
+ test('editing toggled to true, has stored data', async () => {
stubStorage('getEditableContentItem').returns({
message: 'stored content',
updated: 0,
});
element.editing = true;
-
- assert.equal(element._newContent, 'stored content');
+ await element.updateComplete;
+ assert.equal(element.newContent, 'stored content');
assert.isTrue(dispatchSpy.called);
- assert.equal(dispatchSpy.firstCall.args[0].type, 'show-alert');
+ assert.equal(dispatchSpy.lastCall.args[0].type, 'show-alert');
});
- test('editing toggled to true, has no stored data', () => {
+ test('editing toggled to true, has no stored data', async () => {
stubStorage('getEditableContentItem').returns(null);
element.editing = true;
- assert.equal(element._newContent, 'current content');
+ await element.updateComplete;
+
+ assert.equal(element.newContent, 'current content');
assert.equal(dispatchSpy.firstCall.args[0].type, 'editing-changed');
});
- test('edits are cached', () => {
+ test('edits are cached', async () => {
const storeStub = stubStorage('setEditableContentItem');
const eraseStub = stubStorage('eraseEditableContentItem');
element.editing = true;
- element._newContent = 'new content';
- flush();
+ // Needed because editingChanged resets newContent
+ // We want ediingChanged() to finish before triggering newContentChanged
+ await element.updateComplete;
+
+ element.newContent = 'new content';
+
+ await element.updateComplete;
+
element.storeTask?.flush();
assert.isTrue(storeStub.called);
assert.deepEqual(
- [element.storageKey, element._newContent],
+ [element.storageKey, element.newContent],
storeStub.lastCall.args
);
- element._newContent = '';
- flush();
+ element.newContent = '';
+
+ await element.updateComplete;
+
element.storeTask?.flush();
assert.isTrue(eraseStub.called);
diff --git a/polygerrit-ui/app/elements/shared/gr-editable-label/gr-editable-label.ts b/polygerrit-ui/app/elements/shared/gr-editable-label/gr-editable-label.ts
index 8564751..cd020a9 100644
--- a/polygerrit-ui/app/elements/shared/gr-editable-label/gr-editable-label.ts
+++ b/polygerrit-ui/app/elements/shared/gr-editable-label/gr-editable-label.ts
@@ -19,9 +19,6 @@
import '../../../styles/shared-styles';
import '../gr-button/gr-button';
import '../../shared/gr-autocomplete/gr-autocomplete';
-import {PolymerElement} from '@polymer/polymer/polymer-element';
-import {customElement, property} from '@polymer/decorators';
-import {htmlTemplate} from './gr-editable-label_html';
import {IronDropdownElement} from '@polymer/iron-dropdown/iron-dropdown';
import {PaperInputElementExt} from '../../../types/types';
import {
@@ -30,6 +27,9 @@
} from '../gr-autocomplete/gr-autocomplete';
import {addShortcut, Key} from '../../../utils/dom-util';
import {queryAndAssert} from '../../../utils/common-util';
+import {LitElement, css, html} from 'lit';
+import {customElement, property, query, state} from 'lit/decorators';
+import {sharedStyles} from '../../../styles/shared-styles';
const AWAIT_MAX_ITERS = 10;
const AWAIT_STEP = 5;
@@ -40,53 +40,45 @@
}
}
-export interface GrEditableLabel {
- $: {
- dropdown: IronDropdownElement;
- };
-}
-
@customElement('gr-editable-label')
-export class GrEditableLabel extends PolymerElement {
- static get template() {
- return htmlTemplate;
- }
-
+export class GrEditableLabel extends LitElement {
/**
* Fired when the value is changed.
*
* @event changed
*/
- @property({type: String})
+ @query('#dropdown')
+ dropdown?: IronDropdownElement;
+
+ @property()
labelText = '';
@property({type: Boolean})
editing = false;
- @property({type: String, notify: true, observer: '_updateTitle'})
+ @property()
value?: string;
- @property({type: String})
+ @property()
placeholder = '';
@property({type: Boolean})
readOnly = false;
- @property({type: Boolean, reflectToAttribute: true})
+ @property({type: Boolean, reflect: true})
uppercase = false;
@property({type: Number})
maxLength?: number;
- @property({type: String})
- _inputText = '';
+ /* private but used in test */
+ @state() inputText = '';
// This is used to push the iron-input element up on the page, so
// the input is placed in approximately the same position as the
// trigger.
- @property({type: Number})
- readonly _verticalOffset = -30;
+ @state() readonly verticalOffset = -30;
@property({type: Boolean})
showAsEditPencil = false;
@@ -97,9 +89,139 @@
@property({type: Object})
query: AutocompleteQuery = () => Promise.resolve([]);
- override ready() {
- super.ready();
- this._ensureAttribute('tabindex', '0');
+ static override get styles() {
+ return [
+ sharedStyles,
+ css`
+ :host {
+ align-items: center;
+ display: inline-flex;
+ }
+ :host([uppercase]) label {
+ text-transform: uppercase;
+ }
+ input,
+ label {
+ width: 100%;
+ }
+ label {
+ color: var(--deemphasized-text-color);
+ display: inline-block;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ }
+ label.editable {
+ color: var(--link-color);
+ cursor: pointer;
+ }
+ #dropdown {
+ box-shadow: var(--elevation-level-2);
+ }
+ .inputContainer {
+ background-color: var(--dialog-background-color);
+ padding: var(--spacing-m);
+ }
+ .buttons {
+ display: flex;
+ justify-content: flex-end;
+ padding-top: var(--spacing-l);
+ width: 100%;
+ }
+ .buttons gr-button {
+ margin-left: var(--spacing-m);
+ }
+ paper-input {
+ --paper-input-container: {
+ padding: 0;
+ min-width: 15em;
+ }
+ --paper-input-container-input: {
+ font-size: inherit;
+ }
+ --paper-input-container-focus-color: var(--link-color);
+ }
+ gr-button iron-icon {
+ color: inherit;
+ --iron-icon-height: 18px;
+ --iron-icon-width: 18px;
+ }
+ gr-button.pencil {
+ --gr-button-padding: 0px 0px;
+ }
+ `,
+ ];
+ }
+
+ override render() {
+ this.setAttribute('title', this.computeLabel());
+ return html`${this.renderActivateButton()}
+ <iron-dropdown
+ id="dropdown"
+ .verticalAlign=${'auto'}
+ .horizontalAlign=${'auto'}
+ .verticalOffset=${this.verticalOffset}
+ allowOutsideScroll
+ @iron-overlay-canceled=${this.cancel}
+ >
+ <div class="dropdown-content" slot="dropdown-content">
+ <div class="inputContainer" part="input-container">
+ ${this.renderInputBox()}
+ <div class="buttons">
+ <gr-button link="" id="cancelBtn" @click=${this.cancel}
+ >cancel</gr-button
+ >
+ <gr-button link="" id="saveBtn" @click=${this.save}
+ >save</gr-button
+ >
+ </div>
+ </div>
+ </div>
+ </iron-dropdown>`;
+ }
+
+ private renderActivateButton() {
+ if (this.showAsEditPencil) {
+ return html`<gr-button
+ link=""
+ class="pencil ${this.computeLabelClass()}"
+ @click=${this.showDropdown}
+ title=${this.computeLabel()}
+ ><iron-icon icon="gr-icons:edit"></iron-icon
+ ></gr-button>`;
+ } else {
+ return html`<label
+ class=${this.computeLabelClass()}
+ title=${this.computeLabel()}
+ aria-label=${this.computeLabel()}
+ @click=${this.showDropdown}
+ part="label"
+ >${this.computeLabel()}</label
+ >`;
+ }
+ }
+
+ private renderInputBox() {
+ if (this.autocomplete) {
+ return html`<gr-autocomplete
+ .label=${this.labelText}
+ id="autocomplete"
+ .text=${this.inputText}
+ .query=${this.query}
+ @commit=${this.handleCommit}
+ @text-changed=${(e: CustomEvent) => {
+ this.inputText = e.detail.value;
+ }}
+ >
+ </gr-autocomplete>`;
+ } else {
+ return html`<paper-input
+ id="input"
+ .label=${this.labelText}
+ .maxlength=${this.maxLength}
+ .value=${this.inputText}
+ ></paper-input>`;
+ }
}
/** Called in disconnectedCallback. */
@@ -113,48 +235,55 @@
override connectedCallback() {
super.connectedCallback();
+ if (!this.getAttribute('tabindex')) {
+ this.setAttribute('tabindex', '0');
+ }
+ if (!this.getAttribute('id')) {
+ this.setAttribute('id', 'global');
+ }
this.cleanups.push(
- addShortcut(this, {key: Key.ENTER}, e => this._handleEnter(e))
+ addShortcut(this, {key: Key.ENTER}, e => this.handleEnter(e))
);
this.cleanups.push(
- addShortcut(this, {key: Key.ESC}, e => this._handleEsc(e))
+ addShortcut(this, {key: Key.ESC}, e => this.handleEsc(e))
);
}
- _usePlaceholder(value?: string, placeholder?: string) {
+ private usePlaceholder(value?: string, placeholder?: string) {
return (!value || !value.length) && placeholder;
}
- _computeLabel(value?: string, placeholder?: string): string {
- if (this._usePlaceholder(value, placeholder)) {
- return placeholder!;
+ private computeLabel(): string {
+ const {value, placeholder} = this;
+ if (this.usePlaceholder(value, placeholder)) {
+ return placeholder;
}
return value || '';
}
- _showDropdown() {
+ private showDropdown() {
if (this.readOnly || this.editing) return;
- return this._open().then(() => {
- this._nativeInput.focus();
+ return this.openDropdown().then(() => {
+ this.nativeInput.focus();
const input = this.getInput();
if (!input?.value) return;
- this._nativeInput.setSelectionRange(0, input.value.length);
+ this.nativeInput.setSelectionRange(0, input.value.length);
});
}
open() {
- return this._open().then(() => {
- this._nativeInput.focus();
+ return this.openDropdown().then(() => {
+ this.nativeInput.focus();
});
}
- _open() {
- this.$.dropdown.open();
- this._inputText = this.value || '';
+ private openDropdown() {
+ this.dropdown?.open();
+ this.inputText = this.value || '';
this.editing = true;
return new Promise<void>(resolve => {
- this._awaitOpen(resolve);
+ this.awaitOpen(resolve);
});
}
@@ -162,11 +291,11 @@
* NOTE: (wyatta) Slightly hacky way to listen to the overlay actually
* opening. Eventually replace with a direct way to listen to the overlay.
*/
- _awaitOpen(fn: () => void) {
+ private awaitOpen(fn: () => void) {
let iters = 0;
const step = () => {
setTimeout(() => {
- if (this.$.dropdown.style.display !== 'none') {
+ if (this.dropdown?.style.display !== 'none') {
fn.call(this);
} else if (iters++ < AWAIT_MAX_ITERS) {
step.call(this);
@@ -176,16 +305,17 @@
step.call(this);
}
- _id() {
- return this.getAttribute('id') || 'global';
- }
-
- _save() {
+ private save() {
if (!this.editing) {
return;
}
- this.$.dropdown.close();
- this.value = this._inputText || '';
+ this.dropdown?.close();
+ const input = this.getInput();
+ if (input) {
+ this.value = input.value ?? undefined;
+ } else {
+ this.value = this.inputText || '';
+ }
this.editing = false;
this.dispatchEvent(
new CustomEvent('changed', {
@@ -196,23 +326,22 @@
);
}
- _cancel() {
+ private cancel() {
if (!this.editing) {
return;
}
- this.$.dropdown.close();
+ this.dropdown?.close();
this.editing = false;
- this._inputText = this.value || '';
+ this.inputText = this.value || '';
}
- get _nativeInput(): HTMLInputElement {
- // In Polymer 2 inputElement isn't nativeInput anymore
+ private get nativeInput(): HTMLInputElement {
return (this.getInput()?.$.nativeInput ||
this.getInput()?.inputElement ||
this.getGrAutocomplete()) as HTMLInputElement;
}
- _handleEnter(event: KeyboardEvent) {
+ private handleEnter(event: KeyboardEvent) {
const grAutocomplete = this.getGrAutocomplete();
if (event.composedPath().some(el => el === grAutocomplete)) {
return;
@@ -222,39 +351,36 @@
.composedPath()
.some(element => element === inputContainer);
if (isEventFromInput) {
- this._save();
+ this.save();
}
}
- _handleEsc(event: KeyboardEvent) {
+ private handleEsc(event: KeyboardEvent) {
const inputContainer = queryAndAssert(this, '.inputContainer');
const isEventFromInput = event
.composedPath()
.some(element => element === inputContainer);
if (isEventFromInput) {
- this._cancel();
+ this.cancel();
}
}
- _handleCommit() {
+ private handleCommit() {
this.getInput()?.focus();
}
- _computeLabelClass(readOnly?: boolean, value?: string, placeholder?: string) {
+ private computeLabelClass() {
+ const {readOnly, value, placeholder} = this;
const classes = [];
if (!readOnly) {
classes.push('editable');
}
- if (this._usePlaceholder(value, placeholder)) {
+ if (this.usePlaceholder(value, placeholder)) {
classes.push('placeholder');
}
return classes.join(' ');
}
- _updateTitle(value?: string) {
- this.setAttribute('title', this._computeLabel(value, this.placeholder));
- }
-
getInput(): PaperInputElementExt | null {
return this.shadowRoot!.querySelector<PaperInputElementExt>('#input');
}
diff --git a/polygerrit-ui/app/elements/shared/gr-editable-label/gr-editable-label_html.ts b/polygerrit-ui/app/elements/shared/gr-editable-label/gr-editable-label_html.ts
deleted file mode 100644
index e711e9d..0000000
--- a/polygerrit-ui/app/elements/shared/gr-editable-label/gr-editable-label_html.ts
+++ /dev/null
@@ -1,134 +0,0 @@
-/**
- * @license
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-import {html} from '@polymer/polymer/lib/utils/html-tag';
-
-export const htmlTemplate = html`
- <style include="shared-styles">
- :host {
- align-items: center;
- display: inline-flex;
- }
- :host([uppercase]) label {
- text-transform: uppercase;
- }
- input,
- label {
- width: 100%;
- }
- label {
- color: var(--deemphasized-text-color);
- display: inline-block;
- overflow: hidden;
- text-overflow: ellipsis;
- white-space: nowrap;
- }
- label.editable {
- color: var(--link-color);
- cursor: pointer;
- }
- #dropdown {
- box-shadow: var(--elevation-level-2);
- }
- .inputContainer {
- background-color: var(--dialog-background-color);
- padding: var(--spacing-m);
- }
- .buttons {
- display: flex;
- justify-content: flex-end;
- padding-top: var(--spacing-l);
- width: 100%;
- }
- .buttons gr-button {
- margin-left: var(--spacing-m);
- }
- paper-input {
- --paper-input-container: {
- padding: 0;
- min-width: 15em;
- }
- --paper-input-container-input: {
- font-size: inherit;
- }
- --paper-input-container-focus-color: var(--link-color);
- }
- gr-button iron-icon {
- color: inherit;
- --iron-icon-height: 18px;
- --iron-icon-width: 18px;
- }
- gr-button.pencil {
- --gr-button-padding: 0px 0px;
- }
- </style>
- <template is="dom-if" if="[[!showAsEditPencil]]">
- <label
- class$="[[_computeLabelClass(readOnly, value, placeholder)]]"
- title$="[[_computeLabel(value, placeholder)]]"
- aria-label$="[[_computeLabel(value, placeholder)]]"
- on-click="_showDropdown"
- part="label"
- >[[_computeLabel(value, placeholder)]]</label
- >
- </template>
- <template is="dom-if" if="[[showAsEditPencil]]">
- <gr-button
- link=""
- class$="pencil [[_computeLabelClass(readOnly, value, placeholder)]]"
- on-click="_showDropdown"
- title="[[_computeLabel(value, placeholder)]]"
- ><iron-icon icon="gr-icons:edit"></iron-icon
- ></gr-button>
- </template>
- <iron-dropdown
- id="dropdown"
- vertical-align="auto"
- horizontal-align="auto"
- vertical-offset="[[_verticalOffset]]"
- allow-outside-scroll="true"
- on-iron-overlay-canceled="_cancel"
- >
- <div class="dropdown-content" slot="dropdown-content">
- <div class="inputContainer" part="input-container">
- <template is="dom-if" if="[[!autocomplete]]">
- <paper-input
- id="input"
- label="[[labelText]]"
- maxlength="[[maxLength]]"
- value="{{_inputText}}"
- ></paper-input>
- </template>
- <template is="dom-if" if="[[autocomplete]]">
- <gr-autocomplete
- label="[[labelText]]"
- id="autocomplete"
- text="{{_inputText}}"
- query="[[query]]"
- on-commit="_handleCommit"
- >
- </gr-autocomplete>
- </template>
- <div class="buttons">
- <gr-button link="" id="cancelBtn" on-click="_cancel"
- >cancel</gr-button
- >
- <gr-button link="" id="saveBtn" on-click="_save">save</gr-button>
- </div>
- </div>
- </div>
- </iron-dropdown>
-`;
diff --git a/polygerrit-ui/app/elements/shared/gr-editable-label/gr-editable-label_test.ts b/polygerrit-ui/app/elements/shared/gr-editable-label/gr-editable-label_test.ts
index 014116b..a439d05 100644
--- a/polygerrit-ui/app/elements/shared/gr-editable-label/gr-editable-label_test.ts
+++ b/polygerrit-ui/app/elements/shared/gr-editable-label/gr-editable-label_test.ts
@@ -23,6 +23,7 @@
import {PaperInputElement} from '@polymer/paper-input/paper-input';
import {GrButton} from '../gr-button/gr-button';
import {fixture, html} from '@open-wc/testing-helpers';
+import {IronDropdownElement} from '@polymer/iron-dropdown';
suite('gr-editable-label tests', () => {
let element: GrEditableLabel;
@@ -57,10 +58,8 @@
>
value text
</label>
- <dom-if style="display: none;"><template is="dom-if"></template></dom-if>
- <dom-if style="display: none;"><template is="dom-if"></template></dom-if>
<iron-dropdown
- allow-outside-scroll="true"
+ allowoutsidescroll=""
aria-disabled="false"
aria-hidden="true"
horizontal-align="auto"
@@ -75,10 +74,6 @@
id="input"
tabindex="0"
></paper-input>
- <dom-if style="display: none;"><template is="dom-if"></template>
- </dom-if>
- <dom-if style="display: none;"><template is="dom-if"></template>
- </dom-if>
<div class="buttons">
<gr-button
aria-disabled="false"
@@ -104,29 +99,25 @@
</iron-dropdown>`);
});
- test('element render', () => {
+ test('element render', async () => {
// The dropdown is closed and the label is visible:
- assert.isFalse(element.$.dropdown.opened);
+ const dropdown = queryAndAssert<IronDropdownElement>(element, '#dropdown');
+ assert.isFalse(dropdown.opened);
assert.isTrue(label.classList.contains('editable'));
assert.equal(label.textContent, 'value text');
- const focusSpy = sinon.spy(input, 'focus');
- const showSpy = sinon.spy(element, '_showDropdown');
- MockInteractions.tap(label);
-
- return showSpy.lastCall.returnValue!.then(() => {
- // The dropdown is open (which covers up the label):
- assert.isTrue(element.$.dropdown.opened);
- assert.isTrue(focusSpy.called);
- assert.equal(input.value, 'value text');
- });
+ label.click();
+ await element.updateComplete;
+ // The dropdown is open (which covers up the label):
+ assert.isTrue(dropdown.opened);
+ assert.equal(input.value, 'value text');
});
test('title with placeholder', async () => {
assert.equal(element.title, 'value text');
element.value = '';
- await flush();
+ await element.updateComplete;
assert.equal(element.title, 'label text');
});
@@ -149,7 +140,7 @@
assert.isTrue(element.editing);
assert.isFalse(editedSpy.called);
- element._inputText = 'new text';
+ element.inputText = 'new text';
// Press enter:
MockInteractions.keyDownOn(input, 13, null, 'Enter');
await flush();
@@ -170,7 +161,7 @@
assert.isTrue(element.editing);
assert.isFalse(editedSpy.called);
- element._inputText = 'new text';
+ element.inputText = 'new text';
// Press enter:
MockInteractions.pressAndReleaseKeyOn(
queryAndAssert<GrButton>(element, '#saveBtn'),
@@ -196,7 +187,7 @@
assert.isTrue(element.editing);
assert.isFalse(editedSpy.called);
- element._inputText = 'new text';
+ element.inputText = 'new text';
// Press escape:
MockInteractions.keyDownOn(input, 27, null, 'Escape');
await flush();
@@ -218,7 +209,7 @@
assert.isTrue(element.editing);
assert.isFalse(editedSpy.called);
- element._inputText = 'new text';
+ element.inputText = 'new text';
// Press escape:
MockInteractions.tap(queryAndAssert<GrButton>(element, '#cancelBtn'));
await flush();
@@ -236,7 +227,7 @@
setup(async () => {
element = await fixture<GrEditableLabel>(html`
<gr-editable-label
- read-only
+ readOnly
value="value text"
placeholder="label text"
></gr-editable-label>
@@ -246,13 +237,17 @@
test('disallows edit when read-only', async () => {
// The dropdown is closed.
- assert.isFalse(element.$.dropdown.opened);
- MockInteractions.tap(label);
+ const dropdown = queryAndAssert<IronDropdownElement>(
+ element,
+ '#dropdown'
+ );
+ assert.isFalse(dropdown.opened);
+ label.click();
- await flush();
+ await element.updateComplete;
// The dropdown is still closed.
- assert.isFalse(element.$.dropdown.opened);
+ assert.isFalse(dropdown.opened);
});
test('label is not marked as editable', () => {
diff --git a/polygerrit-ui/app/elements/shared/gr-file-status-chip/gr-file-status-chip.ts b/polygerrit-ui/app/elements/shared/gr-file-status-chip/gr-file-status-chip.ts
index 3f759b5..a2088cc 100644
--- a/polygerrit-ui/app/elements/shared/gr-file-status-chip/gr-file-status-chip.ts
+++ b/polygerrit-ui/app/elements/shared/gr-file-status-chip/gr-file-status-chip.ts
@@ -67,10 +67,10 @@
override render() {
return html` <span
- class="${this._computeStatusClass(this.file)}"
+ class=${this._computeStatusClass(this.file)}
tabindex="0"
- title="${this._computeFileStatusLabel(this.file?.status)}"
- aria-label="${this._computeFileStatusLabel(this.file?.status)}"
+ title=${this._computeFileStatusLabel(this.file?.status)}
+ aria-label=${this._computeFileStatusLabel(this.file?.status)}
>
${this._computeFileStatusLabel(this.file?.status)}
</span>`;
diff --git a/polygerrit-ui/app/elements/shared/gr-formatted-text/gr-formatted-text.ts b/polygerrit-ui/app/elements/shared/gr-formatted-text/gr-formatted-text.ts
index c6f44af..9507620 100644
--- a/polygerrit-ui/app/elements/shared/gr-formatted-text/gr-formatted-text.ts
+++ b/polygerrit-ui/app/elements/shared/gr-formatted-text/gr-formatted-text.ts
@@ -353,7 +353,7 @@
private renderText(content: string, isPre?: boolean): TemplateResult {
return html`
<gr-linked-text
- class="${isPre ? 'pre' : ''}"
+ class=${isPre ? 'pre' : ''}
.config=${this.config}
content=${content}
pre
@@ -364,7 +364,7 @@
private renderInlineText(content: string, isPre?: boolean): TemplateResult {
return html`
<gr-linked-text
- class="${isPre ? 'pre' : ''}"
+ class=${isPre ? 'pre' : ''}
.config=${this.config}
content=${content}
pre
@@ -374,7 +374,7 @@
}
private renderLink(text: string, url: string): TemplateResult {
- return html`<a href="${url}">${text}</a>`;
+ return html`<a href=${url}>${text}</a>`;
}
private renderInlineItem(span: InlineItem): TemplateResult {
diff --git a/polygerrit-ui/app/elements/shared/gr-hovercard-account/gr-hovercard-account.ts b/polygerrit-ui/app/elements/shared/gr-hovercard-account/gr-hovercard-account.ts
index 9d763d0..4493e8d 100644
--- a/polygerrit-ui/app/elements/shared/gr-hovercard-account/gr-hovercard-account.ts
+++ b/polygerrit-ui/app/elements/shared/gr-hovercard-account/gr-hovercard-account.ts
@@ -214,7 +214,7 @@
class="removeReviewerOrCC"
link=""
no-uppercase
- @click="${this.handleRemoveReviewerOrCC}"
+ @click=${this.handleRemoveReviewerOrCC}
>
Remove ${this.computeReviewerOrCCText()}
</gr-button>
@@ -224,7 +224,7 @@
class="changeReviewerOrCC"
link=""
no-uppercase
- @click="${this.handleChangeReviewerOrCCStatus}"
+ @click=${this.handleChangeReviewerOrCCStatus}
>
${this.computeChangeReviewerOrCCText()}
</gr-button>
@@ -237,7 +237,7 @@
<gr-endpoint-decorator name="hovercard-status">
<gr-endpoint-param
name="account"
- .value="${this.account}"
+ .value=${this.account}
></gr-endpoint-param>
</gr-endpoint-decorator>
`;
@@ -247,7 +247,7 @@
return html` <div class="links">
<iron-icon class="linkIcon" icon="gr-icons:link"></iron-icon
><a
- href="${ifDefined(this.computeOwnerChangesLink())}"
+ href=${ifDefined(this.computeOwnerChangesLink())}
@click=${() => {
this.forceHide();
return true;
@@ -258,7 +258,7 @@
}}
>Changes</a
>·<a
- href="${ifDefined(this.computeOwnerDashboardLink())}"
+ href=${ifDefined(this.computeOwnerDashboardLink())}
@click=${() => {
this.forceHide();
return true;
@@ -311,7 +311,7 @@
${lastUpdate
? html` (<gr-date-formatter
withTooltip
- .dateStr="${lastUpdate}"
+ .dateStr=${lastUpdate}
></gr-date-formatter
>)`
: ''}
@@ -328,7 +328,7 @@
class="addToAttentionSet"
link=""
no-uppercase
- @click="${this.handleClickAddToAttentionSet}"
+ @click=${this.handleClickAddToAttentionSet}
>
Add to attention set
</gr-button>
@@ -344,7 +344,7 @@
class="removeFromAttentionSet"
link=""
no-uppercase
- @click="${this.handleClickRemoveFromAttentionSet}"
+ @click=${this.handleClickRemoveFromAttentionSet}
>
Remove from attention set
</gr-button>
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-loader.ts b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-loader.ts
index a1ad05e..52022b7 100644
--- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-loader.ts
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-loader.ts
@@ -253,7 +253,7 @@
plugin: null,
});
}
- console.info(`Plugin ${key} ${state}`);
+ console.debug(`Plugin ${key} ${state}`);
return this._plugins.get(key)!;
}
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 36a7354..b1d3914 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
@@ -267,15 +267,15 @@
hasNeutralStatus(labelInfo, approvalInfo));
return html`<div class="reviewer-row">
<gr-account-chip
- .account="${reviewer}"
- .change="${this.change}"
- .vote="${approvalInfo}"
- .label="${labelInfo}"
+ .account=${reviewer}
+ .change=${this.change}
+ .vote=${approvalInfo}
+ .label=${labelInfo}
>
<gr-vote-chip
slot="vote-chip"
- .vote="${approvalInfo}"
- .label="${labelInfo}"
+ .vote=${approvalInfo}
+ .label=${labelInfo}
circle-shape
></gr-vote-chip
></gr-account-chip>
@@ -291,7 +291,7 @@
<td>
<gr-tooltip-content
has-tooltip
- title="${this._computeValueTooltip(labelInfo, mappedLabel.value)}"
+ title=${this._computeValueTooltip(labelInfo, mappedLabel.value)}
>
<gr-label class="${mappedLabel.className} voteChip font-small">
${mappedLabel.value}
@@ -301,8 +301,8 @@
<td>
<gr-account-label
clickable
- .account="${mappedLabel.account}"
- .change="${change}"
+ .account=${mappedLabel.account}
+ .change=${change}
></gr-account-label>
</td>
<td>${this.renderRemoveVote(mappedLabel.account)}</td>
@@ -327,10 +327,8 @@
<gr-button
link
aria-label="Remove vote"
- @click="${this.onDeleteVote}"
- data-account-id="${ifDefined(
- reviewer._account_id as number | undefined
- )}"
+ @click=${this.onDeleteVote}
+ data-account-id=${ifDefined(reviewer._account_id as number | undefined)}
class="deleteBtn ${this.computeDeleteClass(
reviewer,
this.mutable,
diff --git a/polygerrit-ui/app/elements/shared/gr-labeled-autocomplete/gr-labeled-autocomplete.ts b/polygerrit-ui/app/elements/shared/gr-labeled-autocomplete/gr-labeled-autocomplete.ts
index 753835d..c020a9c 100644
--- a/polygerrit-ui/app/elements/shared/gr-labeled-autocomplete/gr-labeled-autocomplete.ts
+++ b/polygerrit-ui/app/elements/shared/gr-labeled-autocomplete/gr-labeled-autocomplete.ts
@@ -89,12 +89,12 @@
<gr-autocomplete
id="autocomplete"
threshold="0"
- .query="${this.query}"
- ?disabled="${this.disabled}"
- .placeholder="${this.placeholder}"
+ .query=${this.query}
+ ?disabled=${this.disabled}
+ .placeholder=${this.placeholder}
borderless=""
></gr-autocomplete>
- <div id="trigger" @click="${this._handleTriggerClick}">▼</div>
+ <div id="trigger" @click=${this._handleTriggerClick}>▼</div>
</div>
</div>
`;
diff --git a/polygerrit-ui/app/elements/shared/gr-linked-chip/gr-linked-chip.ts b/polygerrit-ui/app/elements/shared/gr-linked-chip/gr-linked-chip.ts
index 801b8bf..ad99406 100644
--- a/polygerrit-ui/app/elements/shared/gr-linked-chip/gr-linked-chip.ts
+++ b/polygerrit-ui/app/elements/shared/gr-linked-chip/gr-linked-chip.ts
@@ -113,10 +113,10 @@
this.transparentBackground
)}"
>
- <a href="${this.href}">
+ <a href=${this.href}>
<gr-limited-text
- .limit="${this.limit}"
- .text="${this.text}"
+ .limit=${this.limit}
+ .text=${this.text}
></gr-limited-text>
</a>
<gr-button
diff --git a/polygerrit-ui/app/elements/shared/gr-overlay/gr-overlay.ts b/polygerrit-ui/app/elements/shared/gr-overlay/gr-overlay.ts
index af0251f..4895674 100644
--- a/polygerrit-ui/app/elements/shared/gr-overlay/gr-overlay.ts
+++ b/polygerrit-ui/app/elements/shared/gr-overlay/gr-overlay.ts
@@ -42,6 +42,9 @@
/**
* @attr {Boolean} with-backdrop - inherited from IronOverlay
+ * @attr {Boolean} always-on-top - inherited from IronOverlay
+ * @attr {Boolean} no-cancel-on-esc-key - inherited from IronOverlay
+ * @attr {Boolean} no-cancel-on-outside-click - inherited from IronOverlay
*/
@customElement('gr-overlay')
export class GrOverlay extends base {
diff --git a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-apis/gr-rest-api-helper.ts b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-apis/gr-rest-api-helper.ts
index e12ac7c..95f3eb1 100644
--- a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-apis/gr-rest-api-helper.ts
+++ b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-apis/gr-rest-api-helper.ts
@@ -304,7 +304,7 @@
const elapsed = endTime - startTime;
const startAt = new Date(startTime);
const endAt = new Date(endTime);
- console.info(
+ console.debug(
[
'HTTP',
status,
diff --git a/polygerrit-ui/app/elements/shared/gr-shell-command/gr-shell-command.ts b/polygerrit-ui/app/elements/shared/gr-shell-command/gr-shell-command.ts
index 6e1b20c..d352583 100644
--- a/polygerrit-ui/app/elements/shared/gr-shell-command/gr-shell-command.ts
+++ b/polygerrit-ui/app/elements/shared/gr-shell-command/gr-shell-command.ts
@@ -81,9 +81,9 @@
return html` <label>${label}</label>
<div class="commandContainer">
<gr-copy-clipboard
- .text="${this.command}"
+ .text=${this.command}
hasTooltip
- buttonTitle="${this.tooltip}"
+ buttonTitle=${this.tooltip}
></gr-copy-clipboard>
</div>`;
}
diff --git a/polygerrit-ui/app/elements/shared/gr-tooltip/gr-tooltip.ts b/polygerrit-ui/app/elements/shared/gr-tooltip/gr-tooltip.ts
index 0e41891..bd4efcd 100644
--- a/polygerrit-ui/app/elements/shared/gr-tooltip/gr-tooltip.ts
+++ b/polygerrit-ui/app/elements/shared/gr-tooltip/gr-tooltip.ts
@@ -92,12 +92,12 @@
return html` <div class="tooltip">
<i
class="arrowPositionBelow arrow"
- style="${styleMap({marginLeft: this.arrowCenterOffset})}"
+ style=${styleMap({marginLeft: this.arrowCenterOffset})}
></i>
${this.text}
<i
class="arrowPositionAbove arrow"
- style="${styleMap({marginLeft: this.arrowCenterOffset})}"
+ style=${styleMap({marginLeft: this.arrowCenterOffset})}
></i>
</div>`;
}
diff --git a/polygerrit-ui/app/elements/shared/gr-vote-chip/gr-vote-chip.ts b/polygerrit-ui/app/elements/shared/gr-vote-chip/gr-vote-chip.ts
index d89ed65..146a01e 100644
--- a/polygerrit-ui/app/elements/shared/gr-vote-chip/gr-vote-chip.ts
+++ b/polygerrit-ui/app/elements/shared/gr-vote-chip/gr-vote-chip.ts
@@ -139,7 +139,7 @@
return html`<gr-tooltip-content
class="container ${this.more ? 'more' : ''}"
- title="${this.computeTooltip()}"
+ title=${this.computeTooltip()}
has-tooltip
>
<div class="vote-chip ${this.computeClass()}">${renderValue}</div>
diff --git a/polygerrit-ui/app/embed/diff/gr-context-controls/gr-context-controls-section.ts b/polygerrit-ui/app/embed/diff/gr-context-controls/gr-context-controls-section.ts
new file mode 100644
index 0000000..77ba8cd
--- /dev/null
+++ b/polygerrit-ui/app/embed/diff/gr-context-controls/gr-context-controls-section.ts
@@ -0,0 +1,113 @@
+/**
+ * @license
+ * Copyright 2022 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+import '../../../elements/shared/gr-button/gr-button';
+import {html, LitElement} from 'lit';
+import {customElement, property, state} from 'lit/decorators';
+import {DiffInfo, DiffViewMode, RenderPreferences} from '../../../api/diff';
+import {GrDiffGroup, GrDiffGroupType} from '../gr-diff/gr-diff-group';
+import {diffClasses} from '../gr-diff/gr-diff-utils';
+import {getShowConfig} from './gr-context-controls';
+import {ifDefined} from 'lit/directives/if-defined';
+
+@customElement('gr-context-controls-section')
+export class GrContextControlsSection extends LitElement {
+ /** Should context controls be rendered for expanding above the section? */
+ @property({type: Boolean}) showAbove = false;
+
+ /** Should context controls be rendered for expanding below the section? */
+ @property({type: Boolean}) showBelow = false;
+
+ @property({type: Object}) viewMode = DiffViewMode.SIDE_BY_SIDE;
+
+ /** Must be of type GrDiffGroupType.CONTEXT_CONTROL. */
+ @property({type: Object})
+ group?: GrDiffGroup;
+
+ @property({type: Object})
+ diff?: DiffInfo;
+
+ @property({type: Object})
+ renderPrefs?: RenderPreferences;
+
+ /**
+ * Semantic DOM diff testing does not work with just table fragments, so when
+ * running such tests the render() method has to wrap the DOM in a proper
+ * <table> element.
+ */
+ @state()
+ addTableWrapperForTesting = false;
+
+ /**
+ * The browser API for handling selection does not (yet) work for selection
+ * across multiple shadow DOM elements. So we are rendering gr-diff components
+ * into the light DOM instead of the shadow DOM by overriding this method,
+ * which was the recommended workaround by the lit team.
+ * See also https://github.com/WICG/webcomponents/issues/79.
+ */
+ override createRenderRoot() {
+ return this;
+ }
+
+ private renderPaddingRow(whereClass: 'above' | 'below') {
+ if (!this.showAbove && whereClass === 'above') return;
+ if (!this.showBelow && whereClass === 'below') return;
+ const sideBySide = this.viewMode === DiffViewMode.SIDE_BY_SIDE;
+ const modeClass = sideBySide ? 'side-by-side' : 'unified';
+ const type = sideBySide ? GrDiffGroupType.CONTEXT_CONTROL : undefined;
+ return html`
+ <tr
+ class=${diffClasses('contextBackground', modeClass, whereClass)}
+ left-type=${ifDefined(type)}
+ right-type=${ifDefined(type)}
+ >
+ <td class=${diffClasses('blame')} data-line-number="0"></td>
+ <td class=${diffClasses('contextLineNum')}></td>
+ ${sideBySide ? html`<td class=${diffClasses()}></td>` : ''}
+ <td class=${diffClasses('contextLineNum')}></td>
+ <td class=${diffClasses()}></td>
+ </tr>
+ `;
+ }
+
+ private createContextControlRow() {
+ const sideBySide = this.viewMode === DiffViewMode.SIDE_BY_SIDE;
+ const showConfig = getShowConfig(this.showAbove, this.showBelow);
+ return html`
+ <tr class=${diffClasses('dividerRow', `show-${showConfig}`)}>
+ <td class=${diffClasses('blame')} data-line-number="0"></td>
+ ${sideBySide ? html`<td class=${diffClasses()}></td>` : ''}
+ <td class=${diffClasses('dividerCell')} colspan="3">
+ <gr-context-controls
+ .diff=${this.diff}
+ .renderPreferences=${this.renderPrefs}
+ .group=${this.group}
+ .showConfig=${showConfig}
+ >
+ </gr-context-controls>
+ </td>
+ </tr>
+ `;
+ }
+
+ override render() {
+ const rows = html`
+ ${this.renderPaddingRow('above')} ${this.createContextControlRow()}
+ ${this.renderPaddingRow('below')}
+ `;
+ if (this.addTableWrapperForTesting) {
+ return html`<table>
+ ${rows}
+ </table>`;
+ }
+ return rows;
+ }
+}
+
+declare global {
+ interface HTMLElementTagNameMap {
+ 'gr-context-controls-section': GrContextControlsSection;
+ }
+}
diff --git a/polygerrit-ui/app/embed/diff/gr-context-controls/gr-context-controls-section_test.ts b/polygerrit-ui/app/embed/diff/gr-context-controls/gr-context-controls-section_test.ts
new file mode 100644
index 0000000..2c1043d
--- /dev/null
+++ b/polygerrit-ui/app/embed/diff/gr-context-controls/gr-context-controls-section_test.ts
@@ -0,0 +1,63 @@
+/**
+ * @license
+ * Copyright 2022 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+import '../../../test/common-test-setup-karma';
+import './gr-context-controls-section';
+import {GrContextControlsSection} from './gr-context-controls-section';
+import {html} from 'lit';
+import {fixture} from '@open-wc/testing-helpers';
+
+suite('gr-context-controls-section test', () => {
+ let element: GrContextControlsSection;
+
+ setup(async () => {
+ element = await fixture<GrContextControlsSection>(
+ html`<gr-context-controls-section></gr-context-controls-section>`
+ );
+ element.addTableWrapperForTesting = true;
+ await element.updateComplete;
+ });
+
+ test('render: normal with showAbove and showBelow', async () => {
+ element.showAbove = true;
+ element.showBelow = true;
+ await element.updateComplete;
+ expect(element).lightDom.to.equal(/* HTML */ `
+ <table>
+ <tbody>
+ <tr
+ class="above contextBackground gr-diff side-by-side style-scope"
+ left-type="contextControl"
+ right-type="contextControl"
+ >
+ <td class="blame gr-diff style-scope" data-line-number="0"></td>
+ <td class="contextLineNum gr-diff style-scope"></td>
+ <td class="gr-diff style-scope"></td>
+ <td class="contextLineNum gr-diff style-scope"></td>
+ <td class="gr-diff style-scope"></td>
+ </tr>
+ <tr class="dividerRow gr-diff show-both style-scope">
+ <td class="blame gr-diff style-scope" data-line-number="0"></td>
+ <td class="gr-diff style-scope"></td>
+ <td class="dividerCell gr-diff style-scope" colspan="3">
+ <gr-context-controls showconfig="both"> </gr-context-controls>
+ </td>
+ </tr>
+ <tr
+ class="below contextBackground gr-diff side-by-side style-scope"
+ left-type="contextControl"
+ right-type="contextControl"
+ >
+ <td class="blame gr-diff style-scope" data-line-number="0"></td>
+ <td class="contextLineNum gr-diff style-scope"></td>
+ <td class="gr-diff style-scope"></td>
+ <td class="contextLineNum gr-diff style-scope"></td>
+ <td class="gr-diff style-scope"></td>
+ </tr>
+ </tbody>
+ </table>
+ `);
+ });
+});
diff --git a/polygerrit-ui/app/embed/diff/gr-context-controls/gr-context-controls.ts b/polygerrit-ui/app/embed/diff/gr-context-controls/gr-context-controls.ts
index 0231967..a451700 100644
--- a/polygerrit-ui/app/embed/diff/gr-context-controls/gr-context-controls.ts
+++ b/polygerrit-ui/app/embed/diff/gr-context-controls/gr-context-controls.ts
@@ -80,6 +80,19 @@
export type GrContextControlsShowConfig = 'above' | 'below' | 'both';
+export function getShowConfig(
+ showAbove: boolean,
+ showBelow: boolean
+): GrContextControlsShowConfig {
+ if (showAbove && !showBelow) return 'above';
+ if (!showAbove && showBelow) return 'below';
+
+ // Note that !showAbove && !showBelow also intentionally returns 'both'.
+ // This means the file is completely collapsed, which is unusual, but at least
+ // happens in one test.
+ return 'both';
+}
+
@customElement('gr-context-controls')
export class GrContextControls extends LitElement {
@property({type: Object}) renderPreferences?: RenderPreferences;
@@ -334,11 +347,11 @@
};
const button = html` <paper-button
- class="${classes}"
- aria-label="${ariaLabel}"
- @click="${expandHandler}"
- @mouseenter="${() => mouseHandler('enter')}"
- @mouseleave="${() => mouseHandler('leave')}"
+ class=${classes}
+ aria-label=${ariaLabel}
+ @click=${expandHandler}
+ @mouseenter=${() => mouseHandler('enter')}
+ @mouseleave=${() => mouseHandler('leave')}
>
<span class="showContext">${text}</span>
${tooltip}
@@ -451,7 +464,7 @@
const position =
buttonType === ContextButtonType.BLOCK_ABOVE ? 'top' : 'bottom';
- return html`<paper-tooltip offset="10" position="${position}"
+ return html`<paper-tooltip offset="10" position=${position}
><div class="breadcrumbTooltip">${tooltipText}</div></paper-tooltip
>`;
}
diff --git a/polygerrit-ui/app/embed/diff/gr-coverage-layer/gr-coverage-layer.ts b/polygerrit-ui/app/embed/diff/gr-coverage-layer/gr-coverage-layer.ts
index f121113..dae5c03 100644
--- a/polygerrit-ui/app/embed/diff/gr-coverage-layer/gr-coverage-layer.ts
+++ b/polygerrit-ui/app/embed/diff/gr-coverage-layer/gr-coverage-layer.ts
@@ -1,29 +1,10 @@
/**
* @license
- * Copyright (C) 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
+ * Copyright 2019 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
*/
-import {PolymerElement} from '@polymer/polymer/polymer-element';
-import {htmlTemplate} from './gr-coverage-layer_html';
+import {Side} from '../../../api/diff';
import {CoverageRange, CoverageType, DiffLayer} from '../../../types/types';
-import {customElement, property} from '@polymer/decorators';
-
-declare global {
- interface HTMLElementTagNameMap {
- 'gr-coverage-layer': GrCoverageLayer;
- }
-}
const TOOLTIP_MAP = new Map([
[CoverageType.COVERED, 'Covered by tests.'],
@@ -32,21 +13,12 @@
[CoverageType.NOT_INSTRUMENTED, 'Not instrumented by any tests.'],
]);
-@customElement('gr-coverage-layer')
-export class GrCoverageLayer extends PolymerElement implements DiffLayer {
- static get template() {
- return htmlTemplate;
- }
-
+export class GrCoverageLayer implements DiffLayer {
/**
* Must be sorted by code_range.start_line.
* Must only contain ranges that match the side.
*/
- @property({type: Array})
- coverageRanges: CoverageRange[] = [];
-
- @property({type: String})
- side?: string;
+ private coverageRanges: CoverageRange[] = [];
/**
* We keep track of the line number from the previous annotate() call,
@@ -56,11 +28,22 @@
* and efficient way for finding the coverage range that matches a given
* line number.
*/
- @property({type: Number})
- _lineNumber = 0;
+ private lastLineNumber = 0;
- @property({type: Number})
- _index = 0;
+ /**
+ * See `lastLineNumber` comment.
+ */
+ private index = 0;
+
+ constructor(private readonly side: Side) {}
+
+ /**
+ * Must be sorted by code_range.start_line.
+ * Must only contain ranges that match the side.
+ */
+ setRanges(ranges: CoverageRange[]) {
+ this.coverageRanges = ranges;
+ }
/**
* Layer method to add annotations to a line.
@@ -87,27 +70,27 @@
// If the line number is smaller than before, then we have to reset our
// algorithm and start searching the coverage ranges from the beginning.
// That happens for example when you expand diff sections.
- if (elementLineNumber < this._lineNumber) {
- this._index = 0;
+ if (elementLineNumber < this.lastLineNumber) {
+ this.index = 0;
}
- this._lineNumber = elementLineNumber;
+ this.lastLineNumber = elementLineNumber;
// We simply loop through all the coverage ranges until we find one that
// matches the line number.
- while (this._index < this.coverageRanges.length) {
- const coverageRange = this.coverageRanges[this._index];
+ while (this.index < this.coverageRanges.length) {
+ const coverageRange = this.coverageRanges[this.index];
// If the line number has moved past the current coverage range, then
// try the next coverage range.
- if (this._lineNumber > coverageRange.code_range.end_line) {
- this._index++;
+ if (this.lastLineNumber > coverageRange.code_range.end_line) {
+ this.index++;
continue;
}
// If the line number has not reached the next coverage range (and the
// range before also did not match), then this line has not been
// instrumented. Nothing to do for this line.
- if (this._lineNumber < coverageRange.code_range.start_line) {
+ if (this.lastLineNumber < coverageRange.code_range.start_line) {
return;
}
diff --git a/polygerrit-ui/app/embed/diff/gr-coverage-layer/gr-coverage-layer_html.ts b/polygerrit-ui/app/embed/diff/gr-coverage-layer/gr-coverage-layer_html.ts
deleted file mode 100644
index 1489006..0000000
--- a/polygerrit-ui/app/embed/diff/gr-coverage-layer/gr-coverage-layer_html.ts
+++ /dev/null
@@ -1,19 +0,0 @@
-/**
- * @license
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-import {html} from '@polymer/polymer/lib/utils/html-tag';
-
-export const htmlTemplate = html``;
diff --git a/polygerrit-ui/app/embed/diff/gr-coverage-layer/gr-coverage-layer_test.js b/polygerrit-ui/app/embed/diff/gr-coverage-layer/gr-coverage-layer_test.js
deleted file mode 100644
index e886e61..0000000
--- a/polygerrit-ui/app/embed/diff/gr-coverage-layer/gr-coverage-layer_test.js
+++ /dev/null
@@ -1,124 +0,0 @@
-/**
- * @license
- * Copyright (C) 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-import '../../../test/common-test-setup-karma.js';
-import '../gr-diff/gr-diff-line.js';
-import './gr-coverage-layer.js';
-
-const basicFixture = fixtureFromElement('gr-coverage-layer');
-
-suite('gr-coverage-layer', () => {
- let element;
-
- setup(() => {
- const initialCoverageRanges = [
- {
- type: 'COVERED',
- side: 'right',
- code_range: {
- start_line: 1,
- end_line: 2,
- },
- },
- {
- type: 'NOT_COVERED',
- side: 'right',
- code_range: {
- start_line: 3,
- end_line: 4,
- },
- },
- {
- type: 'PARTIALLY_COVERED',
- side: 'right',
- code_range: {
- start_line: 5,
- end_line: 6,
- },
- },
- {
- type: 'NOT_INSTRUMENTED',
- side: 'right',
- code_range: {
- start_line: 8,
- end_line: 9,
- },
- },
- ];
-
- element = basicFixture.instantiate();
- element.coverageRanges = initialCoverageRanges;
- element.side = 'right';
- });
-
- suite('annotate', () => {
- function createLine(lineNumber) {
- const lineEl = document.createElement('div');
- lineEl.setAttribute('data-side', 'right');
- lineEl.setAttribute('data-value', lineNumber);
- lineEl.className = 'right';
- return lineEl;
- }
-
- function checkLine(lineNumber, className, opt_negated) {
- const line = createLine(lineNumber);
- element.annotate(undefined, line, undefined);
- let contains = line.classList.contains(className);
- if (opt_negated) contains = !contains;
- assert.isTrue(contains);
- }
-
- test('line 1-2 are covered', () => {
- checkLine(1, 'COVERED');
- checkLine(2, 'COVERED');
- });
-
- test('line 3-4 are not covered', () => {
- checkLine(3, 'NOT_COVERED');
- checkLine(4, 'NOT_COVERED');
- });
-
- test('line 5-6 are partially covered', () => {
- checkLine(5, 'PARTIALLY_COVERED');
- checkLine(6, 'PARTIALLY_COVERED');
- });
-
- test('line 7 is implicitly not instrumented', () => {
- checkLine(7, 'COVERED', true);
- checkLine(7, 'NOT_COVERED', true);
- checkLine(7, 'PARTIALLY_COVERED', true);
- checkLine(7, 'NOT_INSTRUMENTED', true);
- });
-
- test('line 8-9 are not instrumented', () => {
- checkLine(8, 'NOT_INSTRUMENTED');
- checkLine(9, 'NOT_INSTRUMENTED');
- });
-
- test('coverage correct, if annotate is called out of order', () => {
- checkLine(8, 'NOT_INSTRUMENTED');
- checkLine(1, 'COVERED');
- checkLine(5, 'PARTIALLY_COVERED');
- checkLine(3, 'NOT_COVERED');
- checkLine(6, 'PARTIALLY_COVERED');
- checkLine(4, 'NOT_COVERED');
- checkLine(9, 'NOT_INSTRUMENTED');
- checkLine(2, 'COVERED');
- });
- });
-});
-
diff --git a/polygerrit-ui/app/embed/diff/gr-coverage-layer/gr-coverage-layer_test.ts b/polygerrit-ui/app/embed/diff/gr-coverage-layer/gr-coverage-layer_test.ts
new file mode 100644
index 0000000..5687b10
--- /dev/null
+++ b/polygerrit-ui/app/embed/diff/gr-coverage-layer/gr-coverage-layer_test.ts
@@ -0,0 +1,113 @@
+/**
+ * @license
+ * Copyright 2019 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+import '../../../test/common-test-setup-karma';
+import {CoverageRange, CoverageType, Side} from '../../../api/diff';
+import {GrCoverageLayer} from './gr-coverage-layer';
+
+suite('gr-coverage-layer', () => {
+ let layer: GrCoverageLayer;
+
+ setup(() => {
+ const initialCoverageRanges: CoverageRange[] = [
+ {
+ type: CoverageType.COVERED,
+ side: Side.RIGHT,
+ code_range: {
+ start_line: 1,
+ end_line: 2,
+ },
+ },
+ {
+ type: CoverageType.NOT_COVERED,
+ side: Side.RIGHT,
+ code_range: {
+ start_line: 3,
+ end_line: 4,
+ },
+ },
+ {
+ type: CoverageType.PARTIALLY_COVERED,
+ side: Side.RIGHT,
+ code_range: {
+ start_line: 5,
+ end_line: 6,
+ },
+ },
+ {
+ type: CoverageType.NOT_INSTRUMENTED,
+ side: Side.RIGHT,
+ code_range: {
+ start_line: 8,
+ end_line: 9,
+ },
+ },
+ ];
+
+ layer = new GrCoverageLayer(Side.RIGHT);
+ layer.setRanges(initialCoverageRanges);
+ });
+
+ suite('annotate', () => {
+ function createLine(lineNumber: number) {
+ const lineEl = document.createElement('div');
+ lineEl.setAttribute('data-side', Side.RIGHT);
+ lineEl.setAttribute('data-value', lineNumber.toString());
+ lineEl.className = Side.RIGHT;
+ return lineEl;
+ }
+
+ function checkLine(
+ lineNumber: number,
+ className: string,
+ negated?: boolean
+ ) {
+ const content = document.createElement('div');
+ const line = createLine(lineNumber);
+ layer.annotate(content, line);
+ let contains = line.classList.contains(className);
+ if (negated) contains = !contains;
+ assert.isTrue(contains);
+ }
+
+ test('line 1-2 are covered', () => {
+ checkLine(1, 'COVERED');
+ checkLine(2, 'COVERED');
+ });
+
+ test('line 3-4 are not covered', () => {
+ checkLine(3, 'NOT_COVERED');
+ checkLine(4, 'NOT_COVERED');
+ });
+
+ test('line 5-6 are partially covered', () => {
+ checkLine(5, 'PARTIALLY_COVERED');
+ checkLine(6, 'PARTIALLY_COVERED');
+ });
+
+ test('line 7 is implicitly not instrumented', () => {
+ checkLine(7, 'COVERED', true);
+ checkLine(7, 'NOT_COVERED', true);
+ checkLine(7, 'PARTIALLY_COVERED', true);
+ checkLine(7, 'NOT_INSTRUMENTED', true);
+ });
+
+ test('line 8-9 are not instrumented', () => {
+ checkLine(8, 'NOT_INSTRUMENTED');
+ checkLine(9, 'NOT_INSTRUMENTED');
+ });
+
+ test('coverage correct, if annotate is called out of order', () => {
+ checkLine(8, 'NOT_INSTRUMENTED');
+ checkLine(1, 'COVERED');
+ checkLine(5, 'PARTIALLY_COVERED');
+ checkLine(3, 'NOT_COVERED');
+ checkLine(6, 'PARTIALLY_COVERED');
+ checkLine(4, 'NOT_COVERED');
+ checkLine(9, 'NOT_INSTRUMENTED');
+ checkLine(2, 'COVERED');
+ });
+ });
+});
diff --git a/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-builder-element.ts b/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-builder-element.ts
index 6854ef34..27ebe4e 100644
--- a/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-builder-element.ts
+++ b/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-builder-element.ts
@@ -14,10 +14,8 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-import '../gr-coverage-layer/gr-coverage-layer';
import '../gr-diff-processor/gr-diff-processor';
import '../../../elements/shared/gr-hovercard/gr-hovercard';
-import '../gr-ranged-comment-layer/gr-ranged-comment-layer';
import './gr-diff-builder-side-by-side';
import {PolymerElement} from '@polymer/polymer/polymer-element';
import {htmlTemplate} from './gr-diff-builder-element_html';
@@ -27,6 +25,7 @@
import {GrDiffBuilderImage} from './gr-diff-builder-image';
import {GrDiffBuilderUnified} from './gr-diff-builder-unified';
import {GrDiffBuilderBinary} from './gr-diff-builder-binary';
+import {GrDiffBuilderLit} from './gr-diff-builder-lit';
import {CancelablePromise, util} from '../../../scripts/util';
import {customElement, property, observe} from '@polymer/decorators';
import {BlameInfo, ImageInfo} from '../../../types/common';
@@ -63,9 +62,6 @@
export interface GrDiffBuilderElement {
$: {
processor: GrDiffProcessor;
- rangeLayer: GrRangedCommentLayer;
- coverageLayerLeft: GrCoverageLayer;
- coverageLayerRight: GrCoverageLayer;
};
}
@@ -111,6 +107,12 @@
*/
/**
+ * Fired whenever a new chunk of lines has been rendered synchronously.
+ *
+ * @event render-progress
+ */
+
+ /**
* Fired when the diff finishes rendering text content.
*
* @event render-content
@@ -179,24 +181,12 @@
@property({type: Array})
commentRanges: CommentRangeLayer[] = [];
- @property({type: Array})
+ @property({type: Array, observer: 'coverageObserver'})
coverageRanges: CoverageRange[] = [];
@property({type: Boolean})
useNewImageDiffUi = false;
- @property({
- type: Array,
- computed: '_computeLeftCoverageRanges(coverageRanges)',
- })
- _leftCoverageRanges?: CoverageRange[];
-
- @property({
- type: Array,
- computed: '_computeRightCoverageRanges(coverageRanges)',
- })
- _rightCoverageRanges?: CoverageRange[];
-
/**
* The promise last returned from `render()` while the asynchronous
* rendering is running - `null` otherwise. Provides a `cancel()`
@@ -205,6 +195,12 @@
@property({type: Object})
_cancelableRenderPromise: CancelablePromise<unknown> | null = null;
+ private coverageLayerLeft = new GrCoverageLayer(Side.LEFT);
+
+ private coverageLayerRight = new GrCoverageLayer(Side.RIGHT);
+
+ private rangeLayer = new GrRangedCommentLayer();
+
constructor() {
super();
afterNextRender(this, () => {
@@ -231,12 +227,21 @@
return this.querySelector('#diffTable') as HTMLTableElement;
}
- _computeLeftCoverageRanges(coverageRanges: CoverageRange[]) {
- return coverageRanges.filter(range => range && range.side === 'left');
+ @observe('commentRanges.*')
+ rangeObserver() {
+ this.rangeLayer.updateRanges(this.commentRanges);
}
- _computeRightCoverageRanges(coverageRanges: CoverageRange[]) {
- return coverageRanges.filter(range => range && range.side === 'right');
+ coverageObserver(coverageRanges: CoverageRange[]) {
+ const leftRanges = coverageRanges.filter(
+ range => range && range.side === Side.LEFT
+ );
+ this.coverageLayerLeft.setRanges(leftRanges);
+
+ const rightRanges = coverageRanges.filter(
+ range => range && range.side === Side.RIGHT
+ );
+ this.coverageLayerRight.setRanges(rightRanges);
}
render(keyLocations: KeyLocations) {
@@ -301,9 +306,9 @@
this._createIntralineLayer(),
this._createTabIndicatorLayer(),
this._createSpecialCharacterIndicatorLayer(),
- this.$.rangeLayer,
- this.$.coverageLayerLeft,
- this.$.coverageLayerRight,
+ this.rangeLayer,
+ this.coverageLayerLeft,
+ this.coverageLayerRight,
];
if (this.layers) {
@@ -459,13 +464,24 @@
// If the diff is binary, but not an image.
return new GrDiffBuilderBinary(this.diff, localPrefs, this.diffElement);
} else if (this.viewMode === DiffViewMode.SIDE_BY_SIDE) {
- builder = new GrDiffBuilderSideBySide(
- this.diff,
- localPrefs,
- this.diffElement,
- this._layers,
- this.renderPrefs
- );
+ const useLit = this.renderPrefs?.use_lit_components;
+ if (useLit) {
+ builder = new GrDiffBuilderLit(
+ this.diff,
+ localPrefs,
+ this.diffElement,
+ this._layers,
+ this.renderPrefs
+ );
+ } else {
+ builder = new GrDiffBuilderSideBySide(
+ this.diff,
+ localPrefs,
+ this.diffElement,
+ this._layers,
+ this.renderPrefs
+ );
+ }
} else if (this.viewMode === DiffViewMode.UNIFIED) {
builder = new GrDiffBuilderUnified(
this.diff,
@@ -508,6 +524,7 @@
);
this._builder.addGroups(added);
}
+ fireEvent(this, 'render-progress');
}
_createIntralineLayer(): DiffLayer {
diff --git a/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-builder-element_html.ts b/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-builder-element_html.ts
index 573f559..581f0fb 100644
--- a/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-builder-element_html.ts
+++ b/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-builder-element_html.ts
@@ -20,19 +20,5 @@
<div class="contentWrapper">
<slot></slot>
</div>
- <gr-ranged-comment-layer
- id="rangeLayer"
- comment-ranges="[[commentRanges]]"
- ></gr-ranged-comment-layer>
- <gr-coverage-layer
- id="coverageLayerLeft"
- coverage-ranges="[[_leftCoverageRanges]]"
- side="left"
- ></gr-coverage-layer>
- <gr-coverage-layer
- id="coverageLayerRight"
- coverage-ranges="[[_rightCoverageRanges]]"
- side="right"
- ></gr-coverage-layer>
<gr-diff-processor id="processor" groups="{{_groups}}"></gr-diff-processor>
`;
diff --git a/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-builder-legacy.ts b/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-builder-legacy.ts
index 7404bbb..ceadc94 100644
--- a/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-builder-legacy.ts
+++ b/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-builder-legacy.ts
@@ -237,7 +237,8 @@
}
const cell = createElementDiff('td', 'dividerCell');
- cell.setAttribute('colspan', '3');
+ const colspan = this.renderPrefs?.show_sign_col ? '5' : '3';
+ cell.setAttribute('colspan', colspan);
row.appendChild(cell);
const contextControls = createElementDiff(
@@ -271,9 +272,13 @@
row.appendChild(this.createBlameCell(0));
row.appendChild(createElementDiff('td', 'contextLineNum'));
if (viewMode === DiffViewMode.SIDE_BY_SIDE) {
+ row.appendChild(createElementDiff('td', 'sign'));
row.appendChild(createElementDiff('td'));
}
row.appendChild(createElementDiff('td', 'contextLineNum'));
+ if (viewMode === DiffViewMode.SIDE_BY_SIDE) {
+ row.appendChild(createElementDiff('td', 'sign'));
+ }
row.appendChild(createElementDiff('td'));
return row;
@@ -288,6 +293,7 @@
const td = createElementDiff('td');
td.classList.add(side);
if (line.type === GrDiffLineType.BLANK) {
+ td.classList.add('blankLineNum');
return td;
}
if (line.type === GrDiffLineType.BOTH || line.type === type) {
@@ -439,8 +445,13 @@
protected buildMoveControls(group: GrDiffGroup) {
const movedIn = group.adds.length > 0;
- const {numberOfCells, movedOutIndex, movedInIndex, lineNumberCols} =
- this.getMoveControlsConfig();
+ const {
+ numberOfCells,
+ movedOutIndex,
+ movedInIndex,
+ lineNumberCols,
+ signCols,
+ } = this.getMoveControlsConfig();
let controlsClass;
let descriptionIndex;
@@ -461,6 +472,10 @@
cells[index].classList.add('moveControlsLineNumCol');
});
+ if (signCols) {
+ cells[signCols.left].classList.add('sign', 'left');
+ cells[signCols.right].classList.add('sign', 'right');
+ }
const moveRangeHeader = createElementDiff('gr-range-header');
moveRangeHeader.setAttribute('icon', 'gr-icons:move-item');
moveRangeHeader.appendChild(descriptionTextDiv);
diff --git a/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-builder-lit.ts b/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-builder-lit.ts
new file mode 100644
index 0000000..3687747
--- /dev/null
+++ b/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-builder-lit.ts
@@ -0,0 +1,185 @@
+/**
+ * @license
+ * Copyright 2022 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+import {RenderPreferences} from '../../../api/diff';
+import {LineNumber} from '../gr-diff/gr-diff-line';
+import {GrDiffGroup} from '../gr-diff/gr-diff-group';
+import {DiffInfo, DiffPreferencesInfo} from '../../../types/diff';
+import {Side} from '../../../constants/constants';
+import {DiffLayer, notUndefined} from '../../../types/types';
+import {diffClasses} from '../gr-diff/gr-diff-utils';
+import {GrDiffBuilder} from './gr-diff-builder';
+import {BlameInfo} from '../../../types/common';
+import {html, render} from 'lit';
+import {GrDiffSection} from './gr-diff-section';
+import '../gr-context-controls/gr-context-controls';
+import './gr-diff-section';
+import {GrDiffRow} from './gr-diff-row';
+
+/**
+ * Base class for builders that are creating the diff using Lit elements.
+ */
+export class GrDiffBuilderLit extends GrDiffBuilder {
+ constructor(
+ diff: DiffInfo,
+ prefs: DiffPreferencesInfo,
+ outputEl: HTMLElement,
+ layers: DiffLayer[] = [],
+ renderPrefs?: RenderPreferences
+ ) {
+ super(diff, prefs, outputEl, layers, renderPrefs);
+ }
+
+ override getContentTdByLine(
+ lineNumber: LineNumber,
+ side?: Side,
+ _root: Element = this.outputEl
+ ): HTMLTableCellElement | null {
+ if (!side) return null;
+ const row = this.findRow(lineNumber, side);
+ return row?.getContentCell(side) ?? null;
+ }
+
+ override getLineElByNumber(lineNumber: LineNumber, side: Side) {
+ const row = this.findRow(lineNumber, side);
+ return row?.getLineNumberCell(side) ?? null;
+ }
+
+ private findRow(lineNumber?: LineNumber, side?: Side): GrDiffRow | undefined {
+ if (!side || !lineNumber) return undefined;
+ const group = this.findGroup(side, lineNumber);
+ if (!group) return undefined;
+ const section = this.findSection(group);
+ if (!section) return undefined;
+ return section.findRow(side, lineNumber);
+ }
+
+ private getDiffRows() {
+ const sections = [
+ ...this.outputEl.querySelectorAll<GrDiffSection>('gr-diff-section'),
+ ];
+ return sections.map(s => s.getDiffRows()).flat();
+ }
+
+ override getLineNumberRows(): HTMLTableRowElement[] {
+ const rows = this.getDiffRows();
+ return rows.map(r => r.getTableRow()).filter(notUndefined);
+ }
+
+ override getLineNumEls(side: Side): HTMLTableCellElement[] {
+ const rows = this.getDiffRows();
+ return rows.map(r => r.getLineNumberCell(side)).filter(notUndefined);
+ }
+
+ override getBlameTdByLine(lineNumber: number): Element | undefined {
+ return this.findRow(lineNumber, Side.LEFT)?.getBlameCell();
+ }
+
+ override getContentByLine(
+ lineNumber: LineNumber,
+ side?: Side,
+ _root?: HTMLElement
+ ): HTMLElement | null {
+ const cell = this.getContentTdByLine(lineNumber, side);
+ return (cell?.firstChild ?? null) as HTMLElement | null;
+ }
+
+ override renderContentByRange(
+ start: LineNumber,
+ end: LineNumber,
+ side: Side
+ ) {
+ // TODO: Revisit whether there is maybe a more efficient and reliable
+ // approach. renderContentByRange() is only used when layers announce
+ // updates. We have to look deeper into the design of layers anyway. So
+ // let's defer optimizing this code until a refactor of layers in general.
+ const groups = this.getGroupsByLineRange(start, end, side);
+ for (const group of groups) {
+ const section = this.findSection(group);
+ for (const row of section?.getDiffRows() ?? []) {
+ row.requestUpdate();
+ }
+ }
+ }
+
+ private findSection(group?: GrDiffGroup): GrDiffSection | undefined {
+ if (!group) return undefined;
+ const leftClass = `left-${group.lineRange.left.start_line}`;
+ const rightClass = `right-${group.lineRange.right.start_line}`;
+ return (
+ this.outputEl.querySelector<GrDiffSection>(
+ `gr-diff-section.${leftClass}.${rightClass}`
+ ) ?? undefined
+ );
+ }
+
+ override renderBlameByRange(
+ blameInfo: BlameInfo,
+ start: number,
+ end: number
+ ) {
+ for (let lineNumber = start; lineNumber <= end; lineNumber++) {
+ const row = this.findRow(lineNumber, Side.LEFT);
+ if (!row) continue;
+ row.blameInfo = blameInfo;
+ }
+ }
+
+ // TODO: Refactor this such that adding the move controls becomes part of the
+ // lit element.
+ protected override getMoveControlsConfig() {
+ return {
+ numberOfCells: 4, // How many cells does the diff table have?
+ movedOutIndex: 1, // Index of left content column in diff table.
+ movedInIndex: 3, // Index of right content column in diff table.
+ lineNumberCols: [0, 2], // Indices of line number columns in diff table.
+ };
+ }
+
+ protected override buildSectionElement(group: GrDiffGroup) {
+ const leftCl = `left-${group.lineRange.left.start_line}`;
+ const rightCl = `right-${group.lineRange.right.start_line}`;
+ const section = html`
+ <gr-diff-section
+ class="${leftCl} ${rightCl}"
+ .group=${group}
+ .diff=${this._diff}
+ .layers=${this.layers}
+ .diffPrefs=${this._prefs}
+ .renderPrefs=${this.renderPrefs}
+ ></gr-diff-section>
+ `;
+ // TODO: Refactor GrDiffBuilder.emitGroup() and buildSectionElement()
+ // such that we can render directly into the correct container.
+ const tempContainer = document.createElement('div');
+ render(section, tempContainer);
+ return tempContainer.firstElementChild as GrDiffSection;
+ }
+
+ override addColumns(outputEl: HTMLElement, lineNumberWidth: number): void {
+ render(
+ html`
+ <colgroup>
+ <col class=${diffClasses('blame')}></col>
+ <col class=${diffClasses(Side.LEFT)} width=${lineNumberWidth}></col>
+ <col class=${diffClasses(Side.LEFT)}></col>
+ <col class=${diffClasses(Side.RIGHT)} width=${lineNumberWidth}></col>
+ <col class=${diffClasses(Side.RIGHT)}></col>
+ </colgroup>
+ `,
+ outputEl
+ );
+ }
+
+ protected override getNextContentOnSide(
+ _content: HTMLElement,
+ _side: Side
+ ): HTMLElement | null {
+ // TODO: getNextContentOnSide() is not required by lit based rendering.
+ // So let's refactor it to be moved into gr-diff-builder-legacy.
+ console.warn('unimplemented method getNextContentOnSide() called');
+ return null;
+ }
+}
diff --git a/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-builder-side-by-side.ts b/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-builder-side-by-side.ts
index 676c389..a711215 100644
--- a/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-builder-side-by-side.ts
+++ b/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-builder-side-by-side.ts
@@ -16,7 +16,7 @@
*/
import {GrDiffGroup, GrDiffGroupType} from '../gr-diff/gr-diff-group';
import {DiffInfo, DiffPreferencesInfo} from '../../../types/diff';
-import {GrDiffLine, LineNumber} from '../gr-diff/gr-diff-line';
+import {GrDiffLine, GrDiffLineType, LineNumber} from '../gr-diff/gr-diff-line';
import {DiffViewMode, Side} from '../../../constants/constants';
import {DiffLayer} from '../../../types/types';
import {RenderPreferences} from '../../../api/diff';
@@ -36,10 +36,11 @@
protected override getMoveControlsConfig() {
return {
- numberOfCells: 4,
- movedOutIndex: 1,
- movedInIndex: 3,
- lineNumberCols: [0, 2],
+ numberOfCells: 6,
+ movedOutIndex: 2,
+ movedInIndex: 5,
+ lineNumberCols: [0, 3],
+ signCols: {left: 1, right: 4},
};
}
@@ -83,6 +84,8 @@
col.setAttribute('width', lineNumberWidth.toString());
colgroup.appendChild(col);
+ colgroup.appendChild(createElementDiff('col', 'sign left'));
+
// Add left-side content.
colgroup.appendChild(createElementDiff('col', 'left'));
@@ -91,6 +94,8 @@
col.setAttribute('width', lineNumberWidth.toString());
colgroup.appendChild(col);
+ colgroup.appendChild(createElementDiff('col', 'sign right'));
+
// Add right-side content.
colgroup.appendChild(document.createElement('col'));
@@ -120,9 +125,28 @@
) {
const lineNumberEl = this.createLineEl(line, lineNumber, line.type, side);
row.appendChild(lineNumberEl);
+ row.appendChild(this.createSignEl(line, side));
row.appendChild(this.createTextEl(lineNumberEl, line, side));
}
+ private createSignEl(line: GrDiffLine, side: Side): HTMLElement {
+ const td = createElementDiff('td', 'sign');
+ td.classList.add(side);
+ if (line.type === GrDiffLineType.BLANK) {
+ td.classList.add('blank');
+ } else if (line.type === GrDiffLineType.ADD && side === Side.RIGHT) {
+ td.classList.add('add');
+ td.innerText = '+';
+ } else if (line.type === GrDiffLineType.REMOVE && side === Side.LEFT) {
+ td.classList.add('remove');
+ td.innerText = '-';
+ }
+ if (!line.hasIntralineInfo) {
+ td.classList.add('no-intraline-info');
+ }
+ return td;
+ }
+
protected override getNextContentOnSide(
content: HTMLElement,
side: Side
diff --git a/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-builder.ts b/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-builder.ts
index 2a80fdf..4efa238 100644
--- a/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-builder.ts
+++ b/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-builder.ts
@@ -194,7 +194,7 @@
group.element = element;
}
- private getGroupsByLineRange(
+ protected getGroupsByLineRange(
startLine: LineNumber,
endLine: LineNumber,
side: Side
@@ -206,8 +206,9 @@
let endIndex = this.groups.findIndex(group =>
group.containsLine(side, endLine)
);
- // Not all groups may have rendered yet. In that case let's just render
- // *all* groups after `startIndex`.
+ // Not all groups may have been processed yet (i.e. this.groups is still
+ // incomplete). In that case let's just return *all* groups until the end
+ // of the array.
if (endIndex === -1) endIndex = this.groups.length - 1;
// The filter preserves the legacy behavior to only return non-context
// groups
@@ -328,6 +329,7 @@
movedOutIndex: number;
movedInIndex: number;
lineNumberCols: number[];
+ signCols?: {left: number; right: number};
};
/**
diff --git a/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-row.ts b/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-row.ts
new file mode 100644
index 0000000..ae18e59
--- /dev/null
+++ b/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-row.ts
@@ -0,0 +1,368 @@
+/**
+ * @license
+ * Copyright 2022 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+import {html, LitElement, TemplateResult} from 'lit';
+import {customElement, property, state} from 'lit/decorators';
+import {ifDefined} from 'lit/directives/if-defined';
+import {createRef, Ref, ref} from 'lit/directives/ref';
+import {
+ DiffResponsiveMode,
+ Side,
+ LineNumber,
+ DiffLayer,
+} from '../../../api/diff';
+import {BlameInfo} from '../../../types/common';
+import {assertIsDefined} from '../../../utils/common-util';
+import {fire} from '../../../utils/event-util';
+import {getBaseUrl} from '../../../utils/url-util';
+import './gr-diff-text';
+import {GrDiffLine, GrDiffLineType} from '../gr-diff/gr-diff-line';
+import {diffClasses, isResponsive} from '../gr-diff/gr-diff-utils';
+
+@customElement('gr-diff-row')
+export class GrDiffRow extends LitElement {
+ contentLeftRef: Ref<HTMLDivElement> = createRef();
+
+ contentRightRef: Ref<HTMLDivElement> = createRef();
+
+ lineNumberLeftRef: Ref<HTMLTableCellElement> = createRef();
+
+ lineNumberRightRef: Ref<HTMLTableCellElement> = createRef();
+
+ blameCellRef: Ref<HTMLTableCellElement> = createRef();
+
+ tableRowRef: Ref<HTMLTableRowElement> = createRef();
+
+ @property({type: Object})
+ left?: GrDiffLine;
+
+ @property({type: Object})
+ right?: GrDiffLine;
+
+ @property({type: Object})
+ blameInfo?: BlameInfo;
+
+ @property({type: Object})
+ responsiveMode?: DiffResponsiveMode;
+
+ @property({type: Number})
+ tabSize = 2;
+
+ @property({type: Number})
+ lineLength = 80;
+
+ @property({type: Boolean})
+ hideFileCommentButton = false;
+
+ @property({type: Object})
+ layers: DiffLayer[] = [];
+
+ /**
+ * While not visible we are trying to optimize rendering performance by
+ * rendering a simpler version of the diff. Once this has become true it
+ * cannot be set back to false.
+ */
+ @state()
+ isVisible = false;
+
+ /**
+ * Semantic DOM diff testing does not work with just table fragments, so when
+ * running such tests the render() method has to wrap the DOM in a proper
+ * <table> element.
+ */
+ @state()
+ addTableWrapperForTesting = false;
+
+ /**
+ * The browser API for handling selection does not (yet) work for selection
+ * across multiple shadow DOM elements. So we are rendering gr-diff components
+ * into the light DOM instead of the shadow DOM by overriding this method,
+ * which was the recommended workaround by the lit team.
+ * See also https://github.com/WICG/webcomponents/issues/79.
+ */
+ override createRenderRoot() {
+ return this;
+ }
+
+ override updated() {
+ this.updateLayers(Side.LEFT);
+ this.updateLayers(Side.RIGHT);
+ }
+
+ /**
+ * TODO: This needs some refinement, because layers do not detect whether they
+ * have already applied their information, so at the moment all layers would
+ * constantly re-apply their information to the diff in each lit rendering
+ * pass.
+ */
+ private updateLayers(side: Side) {
+ if (!this.isVisible) return;
+ const line = this.line(side);
+ const contentEl = this.contentRef(side).value;
+ const lineNumberEl = this.lineNumberRef(side).value;
+ if (!line || !contentEl || !lineNumberEl) return;
+ for (const layer of this.layers) {
+ if (typeof layer.annotate === 'function') {
+ layer.annotate(contentEl, lineNumberEl, line, side);
+ }
+ }
+ }
+
+ private renderInvisible() {
+ return html`
+ <tr>
+ <td class="style-scope gr-diff blame"></td>
+ <td class="style-scope gr-diff left"></td>
+ <td class="style-scope gr-diff left content">
+ <div>${this.left?.text ?? ''}</div>
+ </td>
+ <td class="style-scope gr-diff right"></td>
+ <td class="style-scope gr-diff right content">
+ <div>${this.right?.text ?? ''}</div>
+ </td>
+ </tr>
+ `;
+ }
+
+ override render() {
+ if (!this.left || !this.right) return;
+ if (!this.isVisible) return this.renderInvisible();
+ const row = html`
+ <tr
+ ${ref(this.tableRowRef)}
+ class=${diffClasses('diff-row', 'side-by-side')}
+ left-type=${this.left.type}
+ right-type=${this.right.type}
+ tabindex="-1"
+ >
+ ${this.renderBlameCell()} ${this.renderLineNumberCell(Side.LEFT)}
+ ${this.renderContentCell(Side.LEFT)}
+ ${this.renderLineNumberCell(Side.RIGHT)}
+ ${this.renderContentCell(Side.RIGHT)}
+ </tr>
+ `;
+ if (this.addTableWrapperForTesting) {
+ return html`<table>
+ ${row}
+ </table>`;
+ }
+ return row;
+ }
+
+ getTableRow(): HTMLTableRowElement | undefined {
+ return this.tableRowRef.value;
+ }
+
+ getLineNumberCell(side: Side): HTMLTableCellElement | undefined {
+ return this.lineNumberRef(side).value;
+ }
+
+ getContentCell(side: Side) {
+ const div = this.contentRef(side)?.value;
+ if (!div) return undefined;
+ return div.parentElement as HTMLTableCellElement;
+ }
+
+ getBlameCell() {
+ return this.blameCellRef.value;
+ }
+
+ private renderBlameCell() {
+ // td.blame has `white-space: pre`, so prettier must not add spaces.
+ // prettier-ignore
+ return html`
+ <td
+ ${ref(this.blameCellRef)}
+ class=${diffClasses('blame')}
+ data-line-number=${this.left?.beforeNumber ?? 0}
+ >${this.renderBlameElement()}</td>
+ `;
+ }
+
+ private renderBlameElement() {
+ const lineNum = this.left?.beforeNumber;
+ const commit = this.blameInfo;
+ if (!lineNum || !commit) return;
+
+ const isStartOfRange = commit.ranges.some(r => r.start === lineNum);
+ const extras: string[] = [];
+ if (isStartOfRange) extras.push('startOfRange');
+ const date = new Date(commit.time * 1000).toLocaleDateString();
+ const shortName = commit.author.split(' ')[0];
+ const url = `${getBaseUrl()}/q/${commit.id}`;
+
+ // td.blame has `white-space: pre`, so prettier must not add spaces.
+ // prettier-ignore
+ return html`<span class=${diffClasses(...extras)}
+ ><a href=${url} class=${diffClasses('blameDate')}>${date}</a
+ ><span class=${diffClasses('blameAuthor')}> ${shortName}</span
+ ><gr-hovercard class=${diffClasses()}>
+ <span class=${diffClasses('blameHoverCard')}>
+ Commit ${commit.id}<br />
+ Author: ${commit.author}<br />
+ Date: ${date}<br />
+ <br />
+ ${commit.commit_msg}
+ </span>
+ </gr-hovercard
+ ></span>`;
+ }
+
+ private renderLineNumberCell(side: Side): TemplateResult {
+ const line = this.line(side);
+ const lineNumber = this.lineNumber(side);
+ if (!line || !lineNumber || line.type === GrDiffLineType.BLANK) {
+ return html`<td
+ ${ref(this.lineNumberRef(side))}
+ class=${diffClasses(side)}
+ ></td>`;
+ }
+
+ return html`<td
+ ${ref(this.lineNumberRef(side))}
+ class=${diffClasses(side, 'lineNum')}
+ data-value=${lineNumber}
+ >
+ ${this.renderLineNumberButton(line, lineNumber, side)}
+ </td>`;
+ }
+
+ private renderLineNumberButton(
+ line: GrDiffLine,
+ lineNumber: LineNumber,
+ side: Side
+ ) {
+ if (this.hideFileCommentButton && lineNumber === 'FILE') return;
+ if (lineNumber === 'LOST') return;
+ // .lineNumButton has `white-space: pre`, so prettier must not add spaces.
+ // prettier-ignore
+ return html`
+ <button
+ class=${diffClasses('lineNumButton', side)}
+ tabindex="-1"
+ data-value=${lineNumber}
+ aria-label=${ifDefined(
+ this.computeLineNumberAriaLabel(line, lineNumber)
+ )}
+ @mouseenter=${() =>
+ fire(this, 'line-mouse-enter', {lineNum: lineNumber, side})}
+ @mouseleave=${() =>
+ fire(this, 'line-mouse-leave', {lineNum: lineNumber, side})}
+ >${lineNumber === 'FILE' ? 'File' : lineNumber.toString()}</button>
+ `;
+ }
+
+ private computeLineNumberAriaLabel(line: GrDiffLine, lineNumber: LineNumber) {
+ if (lineNumber === 'FILE') return 'Add file comment';
+
+ // Add aria-labels for valid line numbers.
+ // For unified diff, this method will be called with number set to 0 for
+ // the empty line number column for added/removed lines. This should not
+ // be announced to the screenreader.
+ if (lineNumber <= 0) return undefined;
+
+ switch (line.type) {
+ case GrDiffLineType.REMOVE:
+ return `${lineNumber} removed`;
+ case GrDiffLineType.ADD:
+ return `${lineNumber} added`;
+ case GrDiffLineType.BOTH:
+ case GrDiffLineType.BLANK:
+ return undefined;
+ }
+ }
+
+ private renderContentCell(side: Side): TemplateResult {
+ const line = this.line(side);
+ const lineNumber = this.lineNumber(side);
+ assertIsDefined(line, 'line');
+ const extras: string[] = [line.type, side];
+ if (line.type !== GrDiffLineType.BLANK) extras.push('content');
+ if (!line.hasIntralineInfo) extras.push('no-intraline-info');
+ if (line.beforeNumber === 'FILE') extras.push('file');
+ if (line.beforeNumber === 'LOST') extras.push('lost');
+
+ // .content has `white-space: pre`, so prettier must not add spaces.
+ // prettier-ignore
+ return html`
+ <td
+ class=${diffClasses(...extras)}
+ @mouseenter=${() => {
+ if (lineNumber)
+ fire(this, 'line-mouse-enter', {lineNum: lineNumber, side});
+ }}
+ @mouseleave=${() => {
+ if (lineNumber)
+ fire(this, 'line-mouse-leave', {lineNum: lineNumber, side});
+ }}
+ >${this.renderText(side)}${this.renderThreadGroup(side, lineNumber)}</td>
+ `;
+ }
+
+ private renderThreadGroup(side: Side, lineNumber?: LineNumber) {
+ if (!lineNumber) return;
+ // TODO: For the LOST line number the convention is that a <tr> will always
+ // be rendered, but it will not be visible, because of all cells being
+ // empty. For this to work with lit-based rendering we may only render a
+ // thread-group and a <slot> when there is a thread using that slot. The
+ // cleanest solution for that is probably introducing a gr-diff-model, where
+ // each diff row can look up or observe comment threads.
+ // .content has `white-space: pre`, so prettier must not add spaces.
+ // prettier-ignore
+ return html`<div class="thread-group" data-side=${side}><slot name="${side}-${lineNumber}"></slot></div>`;
+ }
+
+ private contentRef(side: Side) {
+ return side === Side.LEFT ? this.contentLeftRef : this.contentRightRef;
+ }
+
+ private lineNumberRef(side: Side) {
+ return side === Side.LEFT
+ ? this.lineNumberLeftRef
+ : this.lineNumberRightRef;
+ }
+
+ private lineNumber(side: Side) {
+ return this.line(side)?.lineNumber(side);
+ }
+
+ private line(side: Side) {
+ return side === Side.LEFT ? this.left : this.right;
+ }
+
+ /**
+ * Returns a 'div' element containing the supplied |text| as its innerText,
+ * with '\t' characters expanded to a width determined by |tabSize|, and the
+ * text wrapped at column |lineLimit|, which may be Infinity if no wrapping is
+ * desired.
+ */
+ private renderText(side: Side) {
+ const line = this.line(side);
+ const lineNumber = this.lineNumber(side);
+ if (lineNumber === 'FILE' || lineNumber === 'LOST') return;
+ // prettier-ignore
+ const textElement = line?.text
+ ? html`<gr-diff-text
+ ${ref(this.contentRef(side))}
+ .text=${line?.text}
+ .tabSize=${this.tabSize}
+ .lineLimit=${this.lineLength}
+ .isResponsive=${isResponsive(this.responsiveMode)}
+ ></gr-diff-text>` : '';
+ // .content has `white-space: pre`, so prettier must not add spaces.
+ // prettier-ignore
+ return html`<div
+ class=${diffClasses('contentText', side)}
+ .ariaLabel=${line?.text ?? ''}
+ data-side=${ifDefined(side)}
+ >${textElement}</div>`;
+ }
+}
+
+declare global {
+ interface HTMLElementTagNameMap {
+ 'gr-diff-row': GrDiffRow;
+ }
+}
diff --git a/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-row_test.ts b/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-row_test.ts
new file mode 100644
index 0000000..757d906
--- /dev/null
+++ b/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-row_test.ts
@@ -0,0 +1,195 @@
+/**
+ * @license
+ * Copyright 2022 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+import '../../../test/common-test-setup-karma';
+import './gr-diff-row';
+import {GrDiffRow} from './gr-diff-row';
+import {html} from 'lit';
+import {fixture} from '@open-wc/testing-helpers';
+import {GrDiffLine} from '../gr-diff/gr-diff-line';
+import {GrDiffLineType} from '../../../api/diff';
+
+suite('gr-diff-row test', () => {
+ let element: GrDiffRow;
+
+ setup(async () => {
+ element = await fixture<GrDiffRow>(html`<gr-diff-row></gr-diff-row>`);
+ element.isVisible = true;
+ element.addTableWrapperForTesting = true;
+ await element.updateComplete;
+ });
+
+ test('both', async () => {
+ const line = new GrDiffLine(GrDiffLineType.BOTH, 1, 1);
+ line.text = 'lorem ipsum';
+ element.left = line;
+ element.right = line;
+ await element.updateComplete;
+ expect(element).lightDom.to.equal(/* HTML */ `
+ <table>
+ <tbody>
+ <tr
+ class="diff-row gr-diff side-by-side style-scope"
+ left-type="both"
+ right-type="both"
+ tabindex="-1"
+ >
+ <td class="blame gr-diff style-scope" data-line-number="1"></td>
+ <td class="gr-diff left lineNum style-scope" data-value="1">
+ <button
+ class="gr-diff left lineNumButton style-scope"
+ data-value="1"
+ tabindex="-1"
+ >
+ 1
+ </button>
+ </td>
+ <td class="both content gr-diff left no-intraline-info style-scope">
+ <div
+ aria-label="lorem ipsum"
+ class="contentText gr-diff left style-scope"
+ data-side="left"
+ >
+ <gr-diff-text>lorem ipsum</gr-diff-text>
+ </div>
+ <div class="thread-group" data-side="left">
+ <slot name="left-1"> </slot>
+ </div>
+ </td>
+ <td class="gr-diff lineNum right style-scope" data-value="1">
+ <button
+ class="gr-diff lineNumButton right style-scope"
+ data-value="1"
+ tabindex="-1"
+ >
+ 1
+ </button>
+ </td>
+ <td
+ class="both content gr-diff no-intraline-info right style-scope"
+ >
+ <div
+ aria-label="lorem ipsum"
+ class="contentText gr-diff right style-scope"
+ data-side="right"
+ >
+ <gr-diff-text>lorem ipsum</gr-diff-text>
+ </div>
+ <div class="thread-group" data-side="right">
+ <slot name="right-1"> </slot>
+ </div>
+ </td>
+ </tr>
+ </tbody>
+ </table>
+ `);
+ });
+
+ test('add', async () => {
+ const line = new GrDiffLine(GrDiffLineType.ADD, 0, 1);
+ line.text = 'lorem ipsum';
+ element.left = new GrDiffLine(GrDiffLineType.BLANK);
+ element.right = line;
+ await element.updateComplete;
+ expect(element).lightDom.to.equal(/* HTML */ `
+ <table>
+ <tbody>
+ <tr
+ class="diff-row gr-diff side-by-side style-scope"
+ left-type="blank"
+ right-type="add"
+ tabindex="-1"
+ >
+ <td class="blame gr-diff style-scope" data-line-number="0"></td>
+ <td class="gr-diff left style-scope"></td>
+ <td class="blank gr-diff left no-intraline-info style-scope">
+ <div
+ aria-label=""
+ class="contentText gr-diff left style-scope"
+ data-side="left"
+ ></div>
+ </td>
+ <td class="gr-diff lineNum right style-scope" data-value="1">
+ <button
+ aria-label="1 added"
+ class="gr-diff lineNumButton right style-scope"
+ data-value="1"
+ tabindex="-1"
+ >
+ 1
+ </button>
+ </td>
+ <td class="add content gr-diff no-intraline-info right style-scope">
+ <div
+ aria-label="lorem ipsum"
+ class="contentText gr-diff right style-scope"
+ data-side="right"
+ >
+ <gr-diff-text>lorem ipsum</gr-diff-text>
+ </div>
+ <div class="thread-group" data-side="right">
+ <slot name="right-1"> </slot>
+ </div>
+ </td>
+ </tr>
+ </tbody>
+ </table>
+ `);
+ });
+
+ test('remove', async () => {
+ const line = new GrDiffLine(GrDiffLineType.REMOVE, 1, 0);
+ line.text = 'lorem ipsum';
+ element.left = line;
+ element.right = new GrDiffLine(GrDiffLineType.BLANK);
+ await element.updateComplete;
+ expect(element).lightDom.to.equal(/* HTML */ `
+ <table>
+ <tbody>
+ <tr
+ class="diff-row gr-diff side-by-side style-scope"
+ left-type="remove"
+ right-type="blank"
+ tabindex="-1"
+ >
+ <td class="blame gr-diff style-scope" data-line-number="1"></td>
+ <td class="gr-diff left lineNum style-scope" data-value="1">
+ <button
+ aria-label="1 removed"
+ class="gr-diff left lineNumButton style-scope"
+ data-value="1"
+ tabindex="-1"
+ >
+ 1
+ </button>
+ </td>
+ <td
+ class="content gr-diff left no-intraline-info remove style-scope"
+ >
+ <div
+ aria-label="lorem ipsum"
+ class="contentText gr-diff left style-scope"
+ data-side="left"
+ >
+ <gr-diff-text>lorem ipsum</gr-diff-text>
+ </div>
+ <div class="thread-group" data-side="left">
+ <slot name="left-1"> </slot>
+ </div>
+ </td>
+ <td class="gr-diff right style-scope"></td>
+ <td class="blank gr-diff no-intraline-info right style-scope">
+ <div
+ aria-label=""
+ class="contentText gr-diff right style-scope"
+ data-side="right"
+ ></div>
+ </td>
+ </tr>
+ </tbody>
+ </table>
+ `);
+ });
+});
diff --git a/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-section.ts b/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-section.ts
new file mode 100644
index 0000000..b11d767
--- /dev/null
+++ b/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-section.ts
@@ -0,0 +1,240 @@
+/**
+ * @license
+ * Copyright 2022 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+import {html, LitElement} from 'lit';
+import {customElement, property, state} from 'lit/decorators';
+import {
+ DiffInfo,
+ DiffLayer,
+ DiffViewMode,
+ MovedLinkClickedEventDetail,
+ RenderPreferences,
+ Side,
+ LineNumber,
+ DiffPreferencesInfo,
+} from '../../../api/diff';
+import {GrDiffGroup, GrDiffGroupType} from '../gr-diff/gr-diff-group';
+import {countLines, diffClasses} from '../gr-diff/gr-diff-utils';
+import {GrDiffRow} from './gr-diff-row';
+import '../gr-context-controls/gr-context-controls-section';
+import '../gr-context-controls/gr-context-controls';
+import '../gr-range-header/gr-range-header';
+import './gr-diff-row';
+import {whenVisible} from '../../../utils/dom-util';
+
+@customElement('gr-diff-section')
+export class GrDiffSection extends LitElement {
+ @property({type: Object})
+ group?: GrDiffGroup;
+
+ @property({type: Object})
+ diff?: DiffInfo;
+
+ @property({type: Object})
+ renderPrefs?: RenderPreferences;
+
+ @property({type: Object})
+ diffPrefs?: DiffPreferencesInfo;
+
+ @property({type: Object})
+ layers: DiffLayer[] = [];
+
+ /**
+ * While not visible we are trying to optimize rendering performance by
+ * rendering a simpler version of the diff.
+ */
+ @state()
+ isVisible = false;
+
+ /**
+ * Semantic DOM diff testing does not work with just table fragments, so when
+ * running such tests the render() method has to wrap the DOM in a proper
+ * <table> element.
+ */
+ @state()
+ addTableWrapperForTesting = false;
+
+ /**
+ * The browser API for handling selection does not (yet) work for selection
+ * across multiple shadow DOM elements. So we are rendering gr-diff components
+ * into the light DOM instead of the shadow DOM by overriding this method,
+ * which was the recommended workaround by the lit team.
+ * See also https://github.com/WICG/webcomponents/issues/79.
+ */
+ override createRenderRoot() {
+ return this;
+ }
+
+ override connectedCallback() {
+ super.connectedCallback();
+ // TODO: Refine this obviously simplistic approach to optimized rendering.
+ whenVisible(this.parentElement!, () => (this.isVisible = true), 1000);
+ }
+
+ override render() {
+ if (!this.group) return;
+ const extras: string[] = [];
+ extras.push('section');
+ extras.push(this.group.type);
+ if (this.group.isTotal()) extras.push('total');
+ if (this.group.dueToRebase) extras.push('dueToRebase');
+ if (this.group.moveDetails) extras.push('dueToMove');
+ if (this.group.ignoredWhitespaceOnly) extras.push('ignoredWhitespaceOnly');
+
+ const isControl = this.group.type === GrDiffGroupType.CONTEXT_CONTROL;
+ const pairs = isControl ? [] : this.group.getSideBySidePairs();
+ const body = html`
+ <tbody class=${diffClasses(...extras)}>
+ ${this.renderContextControls()} ${this.renderMoveControls()}
+ ${pairs.map(pair => {
+ const leftCl = `left-${pair.left.lineNumber(Side.LEFT)}`;
+ const rightCl = `right-${pair.right.lineNumber(Side.RIGHT)}`;
+ return html`
+ <gr-diff-row
+ class="${leftCl} ${rightCl}"
+ .left=${pair.left}
+ .right=${pair.right}
+ .layers=${this.layers}
+ .lineLength=${this.diffPrefs?.line_length ?? 80}
+ .tabSize=${this.diffPrefs?.tab_size ?? 2}
+ .isVisible=${this.isVisible}
+ >
+ </gr-diff-row>
+ `;
+ })}
+ </tbody>
+ `;
+ if (this.addTableWrapperForTesting) {
+ return html`<table>
+ ${body}
+ </table>`;
+ }
+ return body;
+ }
+
+ getDiffRows(): GrDiffRow[] {
+ return [...this.querySelectorAll<GrDiffRow>('gr-diff-row')];
+ }
+
+ private renderContextControls() {
+ if (this.group?.type !== GrDiffGroupType.CONTEXT_CONTROL) return;
+
+ const leftStart = this.group.lineRange.left.start_line;
+ const leftEnd = this.group.lineRange.left.end_line;
+ const firstGroupIsSkipped = !!this.group.contextGroups[0].skip;
+ const lastGroupIsSkipped =
+ !!this.group.contextGroups[this.group.contextGroups.length - 1].skip;
+ const lineCountLeft = countLines(this.diff, Side.LEFT);
+ const containsWholeFile = lineCountLeft === leftEnd - leftStart + 1;
+ const showAbove =
+ (leftStart > 1 && !firstGroupIsSkipped) || containsWholeFile;
+ const showBelow = leftEnd < lineCountLeft && !lastGroupIsSkipped;
+
+ return html`
+ <gr-context-controls-section
+ .showAbove=${showAbove}
+ .showBelow=${showBelow}
+ .group=${this.group}
+ .diff=${this.diff}
+ .renderPrefs=${this.renderPrefs}
+ .viewMode=${DiffViewMode.SIDE_BY_SIDE}
+ >
+ </gr-context-controls-section>
+ `;
+ }
+
+ findRow(side: Side, lineNumber: LineNumber): GrDiffRow | undefined {
+ return (
+ this.querySelector<GrDiffRow>(`gr-diff-row.${side}-${lineNumber}`) ??
+ undefined
+ );
+ }
+
+ private renderMoveControls() {
+ if (!this.group?.moveDetails) return;
+ const movedIn = this.group.adds.length > 0;
+ const plainCell = html`<td class=${diffClasses()}></td>`;
+ const lineNumberCell = html`
+ <td class=${diffClasses('moveControlsLineNumCol')}></td>
+ `;
+ const moveCell = html`
+ <td class=${diffClasses('moveHeader')}>
+ <gr-range-header class=${diffClasses()} icon="gr-icons:move-item">
+ ${this.renderMoveDescription(movedIn)}
+ </gr-range-header>
+ </td>
+ `;
+ return html`
+ <tr
+ class=${diffClasses('moveControls', movedIn ? 'movedIn' : 'movedOut')}
+ >
+ ${lineNumberCell} ${movedIn ? plainCell : moveCell} ${lineNumberCell}
+ ${movedIn ? moveCell : plainCell}
+ </tr>
+ `;
+ }
+
+ private renderMoveDescription(movedIn: boolean) {
+ if (this.group?.moveDetails?.range) {
+ const {changed, range} = this.group.moveDetails;
+ const otherSide = movedIn ? Side.LEFT : Side.RIGHT;
+ const andChangedLabel = changed ? 'and changed ' : '';
+ const direction = movedIn ? 'from' : 'to';
+ const textLabel = `Moved ${andChangedLabel}${direction} lines `;
+ return html`
+ <div class=${diffClasses()}>
+ <span class=${diffClasses()}>${textLabel}</span>
+ ${this.renderMovedLineAnchor(range.start, otherSide)}
+ <span class=${diffClasses()}> - </span>
+ ${this.renderMovedLineAnchor(range.end, otherSide)}
+ </div>
+ `;
+ }
+
+ return html`
+ <div class=${diffClasses()}>
+ <span class=${diffClasses()}
+ >${movedIn ? 'Moved in' : 'Moved out'}</span
+ >
+ </div>
+ `;
+ }
+
+ private renderMovedLineAnchor(line: number, side: Side) {
+ const listener = (e: MouseEvent) => {
+ e.preventDefault();
+ this.handleMovedLineAnchorClick(e.target, side, line);
+ };
+ // `href` is not actually used but important for Screen Readers
+ return html`
+ <a class=${diffClasses()} href=${`#${line}`} @click=${listener}
+ >${line}</a
+ >
+ `;
+ }
+
+ private handleMovedLineAnchorClick(
+ anchor: EventTarget | null,
+ side: Side,
+ line: number
+ ) {
+ anchor?.dispatchEvent(
+ new CustomEvent<MovedLinkClickedEventDetail>('moved-link-clicked', {
+ detail: {
+ lineNum: line,
+ side,
+ },
+ composed: true,
+ bubbles: true,
+ })
+ );
+ }
+}
+
+declare global {
+ interface HTMLElementTagNameMap {
+ 'gr-diff-section': GrDiffSection;
+ }
+}
diff --git a/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-section_test.ts b/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-section_test.ts
new file mode 100644
index 0000000..88c0e83
--- /dev/null
+++ b/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-section_test.ts
@@ -0,0 +1,213 @@
+/**
+ * @license
+ * Copyright 2022 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+import '../../../test/common-test-setup-karma';
+import './gr-diff-section';
+import {GrDiffSection} from './gr-diff-section';
+import {html} from 'lit';
+import {fixture} from '@open-wc/testing-helpers';
+import {GrDiffGroup, GrDiffGroupType} from '../gr-diff/gr-diff-group';
+import {GrDiffLine} from '../gr-diff/gr-diff-line';
+import {GrDiffLineType} from '../../../api/diff';
+
+suite('gr-diff-section test', () => {
+ let element: GrDiffSection;
+
+ setup(async () => {
+ element = await fixture<GrDiffSection>(
+ html`<gr-diff-section></gr-diff-section>`
+ );
+ element.addTableWrapperForTesting = true;
+ element.isVisible = true;
+ await element.updateComplete;
+ });
+
+ test('3 normal unchanged rows', async () => {
+ const lines = [
+ new GrDiffLine(GrDiffLineType.BOTH, 1, 1),
+ new GrDiffLine(GrDiffLineType.BOTH, 1, 1),
+ new GrDiffLine(GrDiffLineType.BOTH, 1, 1),
+ ];
+ lines[0].text = 'asdf';
+ lines[1].text = 'qwer';
+ lines[2].text = 'zxcv';
+ const group = new GrDiffGroup({type: GrDiffGroupType.BOTH, lines});
+ element.group = group;
+ await element.updateComplete;
+ expect(element).dom.to.equal(/* HTML */ `
+ <gr-diff-section>
+ <gr-diff-row class="left-1 right-1"> </gr-diff-row>
+ <gr-diff-row class="left-1 right-1"> </gr-diff-row>
+ <gr-diff-row class="left-1 right-1"> </gr-diff-row>
+ <table>
+ <tbody class="both gr-diff section style-scope">
+ <tr
+ class="diff-row gr-diff side-by-side style-scope"
+ left-type="both"
+ right-type="both"
+ tabindex="-1"
+ >
+ <td class="blame gr-diff style-scope" data-line-number="1"></td>
+ <td class="gr-diff left lineNum style-scope" data-value="1">
+ <button
+ class="gr-diff left lineNumButton style-scope"
+ data-value="1"
+ tabindex="-1"
+ >
+ 1
+ </button>
+ </td>
+ <td
+ class="both content gr-diff left no-intraline-info style-scope"
+ >
+ <div
+ aria-label="asdf"
+ class="contentText gr-diff left style-scope"
+ data-side="left"
+ >
+ <gr-diff-text></gr-diff-text>
+ </div>
+ <div class="thread-group" data-side="left">
+ <slot name="left-1"> </slot>
+ </div>
+ </td>
+ <td class="gr-diff lineNum right style-scope" data-value="1">
+ <button
+ class="gr-diff lineNumButton right style-scope"
+ data-value="1"
+ tabindex="-1"
+ >
+ 1
+ </button>
+ </td>
+ <td
+ class="both content gr-diff no-intraline-info right style-scope"
+ >
+ <div
+ aria-label="asdf"
+ class="contentText gr-diff right style-scope"
+ data-side="right"
+ >
+ <gr-diff-text></gr-diff-text>
+ </div>
+ <div class="thread-group" data-side="right">
+ <slot name="right-1"> </slot>
+ </div>
+ </td>
+ </tr>
+ <tr
+ class="diff-row gr-diff side-by-side style-scope"
+ left-type="both"
+ right-type="both"
+ tabindex="-1"
+ >
+ <td class="blame gr-diff style-scope" data-line-number="1"></td>
+ <td class="gr-diff left lineNum style-scope" data-value="1">
+ <button
+ class="gr-diff left lineNumButton style-scope"
+ data-value="1"
+ tabindex="-1"
+ >
+ 1
+ </button>
+ </td>
+ <td
+ class="both content gr-diff left no-intraline-info style-scope"
+ >
+ <div
+ aria-label="qwer"
+ class="contentText gr-diff left style-scope"
+ data-side="left"
+ >
+ <gr-diff-text></gr-diff-text>
+ </div>
+ <div class="thread-group" data-side="left">
+ <slot name="left-1"> </slot>
+ </div>
+ </td>
+ <td class="gr-diff lineNum right style-scope" data-value="1">
+ <button
+ class="gr-diff lineNumButton right style-scope"
+ data-value="1"
+ tabindex="-1"
+ >
+ 1
+ </button>
+ </td>
+ <td
+ class="both content gr-diff no-intraline-info right style-scope"
+ >
+ <div
+ aria-label="qwer"
+ class="contentText gr-diff right style-scope"
+ data-side="right"
+ >
+ <gr-diff-text></gr-diff-text>
+ </div>
+ <div class="thread-group" data-side="right">
+ <slot name="right-1"> </slot>
+ </div>
+ </td>
+ </tr>
+ <tr
+ class="diff-row gr-diff side-by-side style-scope"
+ left-type="both"
+ right-type="both"
+ tabindex="-1"
+ >
+ <td class="blame gr-diff style-scope" data-line-number="1"></td>
+ <td class="gr-diff left lineNum style-scope" data-value="1">
+ <button
+ class="gr-diff left lineNumButton style-scope"
+ data-value="1"
+ tabindex="-1"
+ >
+ 1
+ </button>
+ </td>
+ <td
+ class="both content gr-diff left no-intraline-info style-scope"
+ >
+ <div
+ aria-label="zxcv"
+ class="contentText gr-diff left style-scope"
+ data-side="left"
+ >
+ <gr-diff-text></gr-diff-text>
+ </div>
+ <div class="thread-group" data-side="left">
+ <slot name="left-1"> </slot>
+ </div>
+ </td>
+ <td class="gr-diff lineNum right style-scope" data-value="1">
+ <button
+ class="gr-diff lineNumButton right style-scope"
+ data-value="1"
+ tabindex="-1"
+ >
+ 1
+ </button>
+ </td>
+ <td
+ class="both content gr-diff no-intraline-info right style-scope"
+ >
+ <div
+ aria-label="zxcv"
+ class="contentText gr-diff right style-scope"
+ data-side="right"
+ >
+ <gr-diff-text></gr-diff-text>
+ </div>
+ <div class="thread-group" data-side="right">
+ <slot name="right-1"> </slot>
+ </div>
+ </td>
+ </tr>
+ </tbody>
+ </table>
+ </gr-diff-section>
+ `);
+ });
+});
diff --git a/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-text.ts b/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-text.ts
new file mode 100644
index 0000000..bb37c43
--- /dev/null
+++ b/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-text.ts
@@ -0,0 +1,138 @@
+/**
+ * @license
+ * Copyright 2022 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+import {LitElement, html, TemplateResult} from 'lit';
+import {customElement, property} from 'lit/decorators';
+import {diffClasses} from '../gr-diff/gr-diff-utils';
+
+const SURROGATE_PAIR = /[\uD800-\uDBFF][\uDC00-\uDFFF]/;
+
+const TAB = '\t';
+
+@customElement('gr-diff-text')
+export class GrDiffText extends LitElement {
+ /**
+ * The browser API for handling selection does not (yet) work for selection
+ * across multiple shadow DOM elements. So we are rendering gr-diff components
+ * into the light DOM instead of the shadow DOM by overriding this method,
+ * which was the recommended workaround by the lit team.
+ * See also https://github.com/WICG/webcomponents/issues/79.
+ */
+ override createRenderRoot() {
+ return this;
+ }
+
+ @property({type: String})
+ text = '';
+
+ @property({type: Boolean})
+ isResponsive = false;
+
+ @property({type: Number})
+ tabSize = 2;
+
+ @property({type: Number})
+ lineLimit = 80;
+
+ /** Temporary state while rendering. */
+ private textOffset = 0;
+
+ /** Temporary state while rendering. */
+ private columnPos = 0;
+
+ /** Temporary state while rendering. */
+ private pieces: (string | TemplateResult)[] = [];
+
+ /** Split up the string into tabs, surrogate pairs and regular segments. */
+ override render() {
+ this.textOffset = 0;
+ this.columnPos = 0;
+ this.pieces = [];
+ const splitByTab = this.text.split('\t');
+ for (let i = 0; i < splitByTab.length; i++) {
+ const splitBySurrogate = splitByTab[i].split(SURROGATE_PAIR);
+ for (let j = 0; j < splitBySurrogate.length; j++) {
+ this.renderSegment(splitBySurrogate[j]);
+ if (j < splitBySurrogate.length - 1) {
+ this.renderSurrogatePair();
+ }
+ }
+ if (i < splitByTab.length - 1) {
+ this.renderTab();
+ }
+ }
+ if (this.textOffset !== this.text.length) throw new Error('unfinished');
+ return this.pieces;
+ }
+
+ /** Render regular characters, but insert line breaks appropriately. */
+ private renderSegment(segment: string) {
+ let segmentOffset = 0;
+ while (segmentOffset < segment.length) {
+ const newOffset = Math.min(
+ segment.length,
+ segmentOffset + this.lineLimit - this.columnPos
+ );
+ this.renderString(segment.substring(segmentOffset, newOffset));
+ segmentOffset = newOffset;
+ if (segmentOffset < segment.length && this.columnPos === this.lineLimit) {
+ this.renderLineBreak();
+ }
+ }
+ }
+
+ /** Render regular characters. */
+ private renderString(s: string) {
+ if (s.length === 0) return;
+ this.pieces.push(s);
+ this.textOffset += s.length;
+ this.columnPos += s.length;
+ if (this.columnPos > this.lineLimit) throw new Error('over line limit');
+ }
+
+ /** Render a tab character. */
+ private renderTab() {
+ let tabSize = this.tabSize - (this.columnPos % this.tabSize);
+ if (this.columnPos + tabSize > this.lineLimit) {
+ this.renderLineBreak();
+ tabSize = this.tabSize;
+ }
+ const piece = html`<span
+ class=${diffClasses('tab')}
+ style="tab-size: ${tabSize}; -moz-tab-size: ${tabSize};"
+ >${TAB}</span
+ >`;
+ this.pieces.push(piece);
+ this.textOffset += 1;
+ this.columnPos += tabSize;
+ }
+
+ /** Render a surrogate pair: string length is 2, but is just 1 char. */
+ private renderSurrogatePair() {
+ if (this.columnPos === this.lineLimit) {
+ this.renderLineBreak();
+ }
+ this.pieces.push(this.text.substring(this.textOffset, this.textOffset + 2));
+ this.textOffset += 2;
+ this.columnPos += 1;
+ }
+
+ /** Render a line break, don't advance text offset, reset col position. */
+ private renderLineBreak() {
+ if (this.isResponsive) {
+ this.pieces.push(html`<wbr class=${diffClasses()}></wbr>`);
+ } else {
+ this.pieces.push(html`<span class=${diffClasses('br')}></span>`);
+ }
+ // this.textOffset += 0;
+ this.columnPos = 0;
+ }
+}
+
+declare global {
+ interface HTMLElementTagNameMap {
+ 'gr-diff-text': GrDiffText;
+ }
+}
diff --git a/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-text_test.ts b/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-text_test.ts
new file mode 100644
index 0000000..21c0936
--- /dev/null
+++ b/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-text_test.ts
@@ -0,0 +1,154 @@
+/**
+ * @license
+ * Copyright 2022 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+import '../../../test/common-test-setup-karma';
+import './gr-diff-text';
+import {GrDiffText} from './gr-diff-text';
+import {html} from 'lit';
+import {fixture} from '@open-wc/testing-helpers';
+
+const LINE_BREAK = '<span class="style-scope gr-diff br"></span>';
+
+const TAB = '<span class="" style=""></span>';
+
+const TAB_IGNORE = ['class', 'style'];
+
+suite('gr-diff-text test', () => {
+ let element: GrDiffText;
+
+ setup(async () => {
+ element = await fixture<GrDiffText>(html`<gr-diff-text></gr-diff-text>`);
+ await element.updateComplete;
+ });
+
+ const check = async (
+ text: string,
+ html: string,
+ ignoreAttributes: string[] = []
+ ) => {
+ element.text = text;
+ element.tabSize = 4;
+ element.lineLimit = 10;
+ await element.updateComplete;
+ expect(element).lightDom.to.equal(html, {ignoreAttributes});
+ };
+
+ suite('lit rendering', () => {
+ test('renderText newlines 1', () => {
+ check('abcdef', 'abcdef');
+ check('a'.repeat(20), `aaaaaaaaaa${LINE_BREAK}aaaaaaaaaa`);
+ });
+
+ test('renderText newlines 2', () => {
+ check(
+ '<span class="thumbsup">👍</span>',
+ '<span clas' +
+ LINE_BREAK +
+ 's="thumbsu' +
+ LINE_BREAK +
+ 'p">👍</span' +
+ LINE_BREAK +
+ '>'
+ );
+ });
+
+ test('renderText newlines 3', () => {
+ check(
+ '01234\t56789',
+ '01234' + TAB + '56' + LINE_BREAK + '789',
+ TAB_IGNORE
+ );
+ });
+
+ test('renderText newlines 4', async () => {
+ element.lineLimit = 20;
+ await element.updateComplete;
+ check(
+ '👍'.repeat(58),
+ '👍'.repeat(20) +
+ LINE_BREAK +
+ '👍'.repeat(20) +
+ LINE_BREAK +
+ '👍'.repeat(18)
+ );
+ });
+
+ test('tab wrapper style', async () => {
+ for (const size of [1, 3, 8, 55]) {
+ element.tabSize = size;
+ await element.updateComplete;
+ check(
+ '\t',
+ /* HTML */ `
+ <span
+ class="style-scope gr-diff tab"
+ style="tab-size: ${size}; -moz-tab-size: ${size};"
+ >
+ </span>
+ `
+ );
+ }
+ });
+
+ test('tab wrapper insertion', () => {
+ check('abc\tdef', 'abc' + TAB + 'def', TAB_IGNORE);
+ });
+
+ test('escaping HTML', async () => {
+ element.lineLimit = 100;
+ await element.updateComplete;
+ check(
+ '<script>alert("XSS");<' + '/script>',
+ '<script>alert("XSS");</script>'
+ );
+ check('& < > " \' / `', '& < > " \' / `');
+ });
+
+ test('text length with tabs and unicode', async () => {
+ async function expectTextLength(
+ text: string,
+ tabSize: number,
+ expected: number
+ ) {
+ element.text = text;
+ element.tabSize = tabSize;
+ element.lineLimit = expected;
+ await element.updateComplete;
+ const result = element.innerHTML;
+
+ // Must not contain a line break.
+ assert.isNotOk(element.querySelector('span.br'));
+
+ // Increasing the line limit by 1 should not change anything.
+ element.lineLimit = expected + 1;
+ await element.updateComplete;
+ const resultPlusOne = element.innerHTML;
+ assert.equal(resultPlusOne, result);
+
+ // Increasing the line limit to infinity should not change anything.
+ element.lineLimit = Infinity;
+ await element.updateComplete;
+ const resultInf = element.innerHTML;
+ assert.equal(resultInf, result);
+
+ // Decreasing the line limit by 1 should introduce a line break.
+ element.lineLimit = expected + 1;
+ await element.updateComplete;
+ assert.isNotOk(element.querySelector('span.br'));
+ }
+ expectTextLength('12345', 4, 5);
+ expectTextLength('\t\t12', 4, 10);
+ expectTextLength('abc💢123', 4, 7);
+ expectTextLength('abc\t', 8, 8);
+ expectTextLength('abc\t\t', 10, 20);
+ expectTextLength('', 10, 0);
+ // 17 Thai combining chars.
+ expectTextLength('ก้้้้้้้้้้้้้้้้', 4, 17);
+ expectTextLength('abc\tde', 10, 12);
+ expectTextLength('abc\tde\t', 10, 20);
+ expectTextLength('\t\t\t\t\t', 20, 100);
+ });
+ });
+});
diff --git a/polygerrit-ui/app/embed/diff/gr-diff-cursor/gr-diff-cursor.ts b/polygerrit-ui/app/embed/diff/gr-diff-cursor/gr-diff-cursor.ts
index 35b89ec..4ecdcb0 100644
--- a/polygerrit-ui/app/embed/diff/gr-diff-cursor/gr-diff-cursor.ts
+++ b/polygerrit-ui/app/embed/diff/gr-diff-cursor/gr-diff-cursor.ts
@@ -228,6 +228,7 @@
path?: string,
intentionalMove?: boolean
) {
+ this._updateStops();
const row = this._findRowByNumberAndFile(number, side, path);
if (row) {
this.side = side;
@@ -335,6 +336,10 @@
this.preventAutoScrollOnManualScroll = true;
};
+ private _boundHandleDiffRenderProgress = () => {
+ this._updateStops();
+ };
+
private _boundHandleDiffRenderContent = () => {
this._updateStops();
// When done rendering, turn focus on move and automatic scrolling back on
@@ -546,6 +551,10 @@
);
diff.removeEventListener('render-start', this._boundHandleDiffRenderStart);
diff.removeEventListener(
+ 'render-progress',
+ this._boundHandleDiffRenderProgress
+ );
+ diff.removeEventListener(
'render-content',
this._boundHandleDiffRenderContent
);
@@ -561,6 +570,10 @@
this.boundHandleDiffLoadingChanged
);
diff.addEventListener('render-start', this._boundHandleDiffRenderStart);
+ diff.addEventListener(
+ 'render-progress',
+ this._boundHandleDiffRenderProgress
+ );
diff.addEventListener('render-content', this._boundHandleDiffRenderContent);
diff.addEventListener('line-selected', this._boundHandleDiffLineSelected);
}
diff --git a/polygerrit-ui/app/embed/diff/gr-diff-image-viewer/gr-image-viewer.ts b/polygerrit-ui/app/embed/diff/gr-diff-image-viewer/gr-image-viewer.ts
index c25b284..1068a8d 100644
--- a/polygerrit-ui/app/embed/diff/gr-diff-image-viewer/gr-image-viewer.ts
+++ b/polygerrit-ui/app/embed/diff/gr-diff-image-viewer/gr-image-viewer.ts
@@ -375,15 +375,15 @@
color === this.backgroundColor && !this.checkerboardSelected;
return html`
<div
- class="${classMap({
+ class=${classMap({
'color-picker-button': true,
selected,
- })}"
+ })}
>
<paper-icon-button
class="color"
- style="${styleMap({backgroundColor: color})}"
- @click="${colorPicked}"
+ style=${styleMap({backgroundColor: color})}
+ @click=${colorPicked}
></paper-icon-button>
</div>
`;
@@ -392,14 +392,14 @@
private renderCheckerboardButton() {
return html`
<div
- class="${classMap({
+ class=${classMap({
'color-picker-button': true,
selected: this.checkerboardSelected,
- })}"
+ })}
>
<paper-icon-button
class="color checkerboard"
- @click="${this.pickCheckerboard}"
+ @click=${this.pickCheckerboard}
>
</paper-icon-button>
</div>
@@ -412,14 +412,14 @@
const sourceImage = html`
<img
id="source-image"
- src="${src}"
- class="${classMap({checkerboard: this.checkerboardSelected})}"
- style="${styleMap({
+ src=${src}
+ class=${classMap({checkerboard: this.checkerboardSelected})}
+ style=${styleMap({
backgroundColor: this.checkerboardSelected
? ''
: this.backgroundColor,
- })}"
- @load="${this.updateSizes}"
+ })}
+ @load=${this.updateSizes}
/>
`;
@@ -428,15 +428,15 @@
${sourceImage}
<img
id="highlight-image"
- style="${styleMap({
+ style=${styleMap({
opacity: this.showHighlight ? '1' : '0',
// When the highlight layer is not being shown, saving the image or
// opening it in a new tab from the context menu, e.g. for external
// comparison, should give back the source image, not the highlight
// layer.
'pointer-events': this.showHighlight ? 'auto' : 'none',
- })}"
- src="${ifDefined(this.diffHighlightSrc)}"
+ })}
+ src=${ifDefined(this.diffHighlightSrc)}
/>
</div>
`;
@@ -461,17 +461,14 @@
};
const versionToggle = html`
<div id="version-switcher">
- <paper-button
- class="${classMap(leftClasses)}"
- @click="${this.selectBase}"
- >
+ <paper-button class=${classMap(leftClasses)} @click=${this.selectBase}>
Base
</paper-button>
- <paper-fab mini icon="gr-icons:swapHoriz" @click="${this.manualBlink}">
+ <paper-fab mini icon="gr-icons:swapHoriz" @click=${this.manualBlink}>
</paper-fab>
<paper-button
- class="${classMap(rightClasses)}"
- @click="${this.selectRevision}"
+ class=${classMap(rightClasses)}
+ @click=${this.selectRevision}
>
Revision
</paper-button>
@@ -486,8 +483,8 @@
? html`
<paper-checkbox
id="highlight-changes"
- ?checked="${this.showHighlight}"
- @change="${this.showHighlightChanged}"
+ ?checked=${this.showHighlight}
+ @change=${this.showHighlightChanged}
>
Highlight differences
</paper-checkbox>
@@ -496,17 +493,17 @@
const overviewImage = html`
<gr-overview-image
- .frameRect="${this.overviewFrame}"
- @center-updated="${this.onOverviewCenterUpdated}"
+ .frameRect=${this.overviewFrame}
+ @center-updated=${this.onOverviewCenterUpdated}
>
<img
- src="${src}"
- class="${classMap({checkerboard: this.checkerboardSelected})}"
- style="${styleMap({
+ src=${src}
+ class=${classMap({checkerboard: this.checkerboardSelected})}
+ style=${styleMap({
backgroundColor: this.checkerboardSelected
? ''
: this.backgroundColor,
- })}"
+ })}
/>
</gr-overview-image>
`;
@@ -516,12 +513,12 @@
<paper-listbox
slot="dropdown-content"
selected="fit"
- .attrForSelected="${'value'}"
- @selected-changed="${this.zoomControlChanged}"
+ .attrForSelected=${'value'}
+ @selected-changed=${this.zoomControlChanged}
>
${this.zoomLevels.map(
zoomLevel => html`
- <paper-item value="${zoomLevel}">
+ <paper-item value=${zoomLevel}>
${zoomLevel === 'fit' ? 'Fit' : `${zoomLevel * 100}%`}
</paper-item>
`
@@ -533,8 +530,8 @@
const followMouse = html`
<paper-checkbox
id="follow-mouse"
- ?checked="${this.followMouse}"
- @change="${this.followMouseChanged}"
+ ?checked=${this.followMouse}
+ @change=${this.followMouseChanged}
>
Magnifier follows mouse
</paper-checkbox>
@@ -566,20 +563,20 @@
const spacer = html`
<div
id="spacer"
- style="${styleMap({
+ style=${styleMap({
width: `${spacerWidth}px`,
height: `${spacerHeight}px`,
- })}"
+ })}
></div>
`;
const automaticBlink = html`
<paper-fab
id="automatic-blink-button"
- class="${classMap({show: this.automaticBlinkShown})}"
+ class=${classMap({show: this.automaticBlinkShown})}
title="Automatic blink"
icon="gr-icons:${this.automaticBlink ? 'pause' : 'playArrow'}"
- @click="${this.toggleAutomaticBlink}"
+ @click=${this.toggleAutomaticBlink}
>
</paper-fab>
`;
@@ -609,25 +606,25 @@
${customStyle}
<div
class="imageArea"
- @mousemove="${this.mousemoveImageArea}"
- @mouseleave="${this.mouseleaveImageArea}"
+ @mousemove=${this.mousemoveImageArea}
+ @mouseleave=${this.mouseleaveImageArea}
>
<gr-zoomed-image
- class="${classMap({
+ class=${classMap({
base: this.baseSelected,
revision: !this.baseSelected,
- })}"
- style="${styleMap({
+ })}
+ style=${styleMap({
...this.zoomedImageStyle,
cursor: this.grabbing ? 'grabbing' : 'pointer',
- })}"
- .scale="${this.scale}"
- .frameRect="${this.magnifierFrame}"
- @mousedown="${this.mousedownMagnifier}"
- @mouseup="${this.mouseupMagnifier}"
- @mousemove="${this.mousemoveMagnifier}"
- @mouseleave="${this.mouseleaveMagnifier}"
- @dragstart="${this.dragstartMagnifier}"
+ })}
+ .scale=${this.scale}
+ .frameRect=${this.magnifierFrame}
+ @mousedown=${this.mousedownMagnifier}
+ @mouseup=${this.mouseupMagnifier}
+ @mousemove=${this.mousemoveMagnifier}
+ @mouseleave=${this.mouseleaveMagnifier}
+ @dragstart=${this.dragstartMagnifier}
>
${sourceImageWithHighlight}
</gr-zoomed-image>
diff --git a/polygerrit-ui/app/embed/diff/gr-diff-image-viewer/gr-overview-image.ts b/polygerrit-ui/app/embed/diff/gr-diff-image-viewer/gr-overview-image.ts
index 28e6d82..49a0eb5 100644
--- a/polygerrit-ui/app/embed/diff/gr-diff-image-viewer/gr-overview-image.ts
+++ b/polygerrit-ui/app/embed/diff/gr-diff-image-viewer/gr-overview-image.ts
@@ -124,26 +124,26 @@
<div class="content-box">
<div
class="content"
- style="${styleMap({
+ style=${styleMap({
...this.contentStyle,
- })}"
- @mousemove="${this.maybeDragFrame}"
+ })}
+ @mousemove=${this.maybeDragFrame}
@mousedown=${this.clickOverview}
- @mouseup="${this.releaseFrame}"
+ @mouseup=${this.releaseFrame}
>
<div
class="content-transform"
- style="${styleMap(this.contentTransformStyle)}"
+ style=${styleMap(this.contentTransformStyle)}
>
<slot></slot>
</div>
<div
class="frame"
- style="${styleMap({
+ style=${styleMap({
...this.frameStyle,
cursor: this.dragging ? 'grabbing' : 'grab',
- })}"
- @mousedown="${this.grabFrame}"
+ })}
+ @mousedown=${this.grabFrame}
></div>
</div>
</div>
diff --git a/polygerrit-ui/app/embed/diff/gr-diff-image-viewer/gr-zoomed-image.ts b/polygerrit-ui/app/embed/diff/gr-diff-image-viewer/gr-zoomed-image.ts
index 66d4671..6fdec67 100644
--- a/polygerrit-ui/app/embed/diff/gr-diff-image-viewer/gr-zoomed-image.ts
+++ b/polygerrit-ui/app/embed/diff/gr-diff-image-viewer/gr-zoomed-image.ts
@@ -59,7 +59,7 @@
override render() {
return html`
<div id="clip">
- <div id="transform" style="${styleMap(this.imageStyles)}">
+ <div id="transform" style=${styleMap(this.imageStyles)}>
<slot></slot>
</div>
</div>
diff --git a/polygerrit-ui/app/embed/diff/gr-diff-mode-selector/gr-diff-mode-selector.ts b/polygerrit-ui/app/embed/diff/gr-diff-mode-selector/gr-diff-mode-selector.ts
index 022dbb9..1b53d05 100644
--- a/polygerrit-ui/app/embed/diff/gr-diff-mode-selector/gr-diff-mode-selector.ts
+++ b/polygerrit-ui/app/embed/diff/gr-diff-mode-selector/gr-diff-mode-selector.ts
@@ -17,39 +17,31 @@
import {Subscription} from 'rxjs';
import '@polymer/iron-icon/iron-icon';
import '@polymer/iron-a11y-announcer/iron-a11y-announcer';
-import '../../../styles/shared-styles';
import '../../../elements/shared/gr-button/gr-button';
import {DiffViewMode} from '../../../constants/constants';
-import {htmlTemplate} from './gr-diff-mode-selector_html';
-import {customElement, property} from '@polymer/decorators';
+import {customElement, property, state} from 'lit/decorators';
import {IronA11yAnnouncer} from '@polymer/iron-a11y-announcer/iron-a11y-announcer';
import {FixIronA11yAnnouncer} from '../../../types/types';
import {getAppContext} from '../../../services/app-context';
import {fireIronAnnounce} from '../../../utils/event-util';
import {browserModelToken} from '../../../models/browser/browser-model';
-import {resolve, DIPolymerElement} from '../../../models/dependency';
+import {resolve} from '../../../models/dependency';
+import {css, html, LitElement} from 'lit';
+import {sharedStyles} from '../../../styles/shared-styles';
@customElement('gr-diff-mode-selector')
-export class GrDiffModeSelector extends DIPolymerElement {
- static get template() {
- return htmlTemplate;
- }
-
- @property({type: String, notify: true})
- mode: DiffViewMode = DiffViewMode.SIDE_BY_SIDE;
-
+export class GrDiffModeSelector extends LitElement {
/**
* If set to true, the user's preference will be updated every time a
* button is tapped. Don't set to true if there is no user.
*/
- @property({type: Boolean})
- saveOnChange = false;
+ @property({type: Boolean}) saveOnChange = false;
- @property({type: Boolean})
- showTooltipBelow = false;
+ @property({type: Boolean}) showTooltipBelow = false;
- // Private but accessed by tests.
- readonly getBrowserModel = resolve(this, browserModelToken);
+ @state() private mode: DiffViewMode = DiffViewMode.SIDE_BY_SIDE;
+
+ private readonly getBrowserModel = resolve(this, browserModelToken);
private readonly userModel = getAppContext().userModel;
@@ -79,18 +71,70 @@
super.disconnectedCallback();
}
+ static override styles = [
+ sharedStyles,
+ css`
+ :host {
+ /* Used to remove horizontal whitespace between the icons. */
+ display: flex;
+ }
+ gr-button.selected iron-icon {
+ color: var(--link-color);
+ }
+ iron-icon {
+ height: 1.3rem;
+ width: 1.3rem;
+ }
+ `,
+ ];
+
+ override render() {
+ return html`
+ <gr-tooltip-content
+ has-tooltip
+ title="Side-by-side diff"
+ ?position-below=${this.showTooltipBelow}
+ >
+ <gr-button
+ id="sideBySideBtn"
+ link
+ class=${this.computeSideBySideSelected()}
+ aria-pressed=${this.isSideBySideSelected()}
+ @click=${this.handleSideBySideTap}
+ >
+ <iron-icon icon="gr-icons:side-by-side"></iron-icon>
+ </gr-button>
+ </gr-tooltip-content>
+ <gr-tooltip-content
+ has-tooltip
+ ?position-below=${this.showTooltipBelow}
+ title="Unified diff"
+ >
+ <gr-button
+ id="unifiedBtn"
+ link
+ class=${this.computeUnifiedSelected()}
+ aria-pressed=${this.isUnifiedSelected()}
+ @click=${this.handleUnifiedTap}
+ >
+ <iron-icon icon="gr-icons:unified"></iron-icon>
+ </gr-button>
+ </gr-tooltip-content>
+ `;
+ }
+
/**
* Set the mode. If save on change is enabled also update the preference.
*/
- setMode(newMode: DiffViewMode) {
+ private setMode(newMode: DiffViewMode) {
if (this.saveOnChange && this.mode && this.mode !== newMode) {
this.userModel.updatePreferences({diff_view: newMode});
}
this.mode = newMode;
let announcement;
- if (this.isUnifiedSelected(newMode)) {
+ if (this.isUnifiedSelected()) {
announcement = 'Changed diff view to unified';
- } else if (this.isSideBySideSelected(newMode)) {
+ } else if (this.isSideBySideSelected()) {
announcement = 'Changed diff view to side by side';
}
if (announcement) {
@@ -98,27 +142,27 @@
}
}
- _computeSideBySideSelected(mode?: DiffViewMode) {
- return mode === DiffViewMode.SIDE_BY_SIDE ? 'selected' : '';
+ private computeSideBySideSelected() {
+ return this.mode === DiffViewMode.SIDE_BY_SIDE ? 'selected' : '';
}
- _computeUnifiedSelected(mode?: DiffViewMode) {
- return mode === DiffViewMode.UNIFIED ? 'selected' : '';
+ private computeUnifiedSelected() {
+ return this.mode === DiffViewMode.UNIFIED ? 'selected' : '';
}
- isSideBySideSelected(mode?: DiffViewMode) {
- return mode === DiffViewMode.SIDE_BY_SIDE;
+ private isSideBySideSelected() {
+ return this.mode === DiffViewMode.SIDE_BY_SIDE;
}
- isUnifiedSelected(mode?: DiffViewMode) {
- return mode === DiffViewMode.UNIFIED;
+ private isUnifiedSelected() {
+ return this.mode === DiffViewMode.UNIFIED;
}
- _handleSideBySideTap() {
+ private handleSideBySideTap() {
this.setMode(DiffViewMode.SIDE_BY_SIDE);
}
- _handleUnifiedTap() {
+ private handleUnifiedTap() {
this.setMode(DiffViewMode.UNIFIED);
}
}
diff --git a/polygerrit-ui/app/embed/diff/gr-diff-mode-selector/gr-diff-mode-selector_html.ts b/polygerrit-ui/app/embed/diff/gr-diff-mode-selector/gr-diff-mode-selector_html.ts
deleted file mode 100644
index 8a6d95d..0000000
--- a/polygerrit-ui/app/embed/diff/gr-diff-mode-selector/gr-diff-mode-selector_html.ts
+++ /dev/null
@@ -1,63 +0,0 @@
-/**
- * @license
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-import {html} from '@polymer/polymer/lib/utils/html-tag';
-
-export const htmlTemplate = html`
- <style include="shared-styles">
- :host {
- /* Used to remove horizontal whitespace between the icons. */
- display: flex;
- }
- gr-button.selected iron-icon {
- color: var(--link-color);
- }
- iron-icon {
- height: 1.3rem;
- width: 1.3rem;
- }
- </style>
- <gr-tooltip-content
- has-tooltip=""
- title="Side-by-side diff"
- position-below="[[showTooltipBelow]]"
- >
- <gr-button
- id="sideBySideBtn"
- link=""
- class$="[[_computeSideBySideSelected(mode)]]"
- aria-pressed$="[[isSideBySideSelected(mode)]]"
- on-click="_handleSideBySideTap"
- >
- <iron-icon icon="gr-icons:side-by-side"></iron-icon>
- </gr-button>
- </gr-tooltip-content>
- <gr-tooltip-content
- has-tooltip=""
- position-below="[[showTooltipBelow]]"
- title="Unified diff"
- >
- <gr-button
- id="unifiedBtn"
- link=""
- class$="[[_computeUnifiedSelected(mode)]]"
- aria-pressed$="[[isUnifiedSelected(mode)]]"
- on-click="_handleUnifiedTap"
- >
- <iron-icon icon="gr-icons:unified"></iron-icon>
- </gr-button>
- </gr-tooltip-content>
-`;
diff --git a/polygerrit-ui/app/embed/diff/gr-diff-mode-selector/gr-diff-mode-selector_test.ts b/polygerrit-ui/app/embed/diff/gr-diff-mode-selector/gr-diff-mode-selector_test.ts
index 3ade907..6ba5533 100644
--- a/polygerrit-ui/app/embed/diff/gr-diff-mode-selector/gr-diff-mode-selector_test.ts
+++ b/polygerrit-ui/app/embed/diff/gr-diff-mode-selector/gr-diff-mode-selector_test.ts
@@ -19,57 +19,151 @@
import './gr-diff-mode-selector';
import {GrDiffModeSelector} from './gr-diff-mode-selector';
import {DiffViewMode} from '../../../constants/constants';
-import {stubUsers} from '../../../test/test-utils';
-
-const basicFixture = fixtureFromElement('gr-diff-mode-selector');
+import {
+ queryAndAssert,
+ stubUsers,
+ waitUntilObserved,
+} from '../../../test/test-utils';
+import {fixture, html} from '@open-wc/testing-helpers';
+import {wrapInProvider} from '../../../models/di-provider-element';
+import {
+ BrowserModel,
+ browserModelToken,
+} from '../../../models/browser/browser-model';
+import {getAppContext} from '../../../services/app-context';
+import {UserModel} from '../../../models/user/user-model';
+import {createPreferences} from '../../../test/test-data-generators';
+import {GrButton} from '../../../elements/shared/gr-button/gr-button';
suite('gr-diff-mode-selector tests', () => {
let element: GrDiffModeSelector;
+ let browserModel: BrowserModel;
+ let userModel: UserModel;
- setup(() => {
- element = basicFixture.instantiate();
+ setup(async () => {
+ userModel = getAppContext().userModel;
+ browserModel = new BrowserModel(userModel);
+ element = (
+ await fixture(
+ wrapInProvider(
+ html`<gr-diff-mode-selector></gr-diff-mode-selector>`,
+ browserModelToken,
+ browserModel
+ )
+ )
+ ).querySelector('gr-diff-mode-selector')!;
});
- test('_computeSelectedClass', () => {
- assert.equal(
- element._computeSideBySideSelected(DiffViewMode.SIDE_BY_SIDE),
- 'selected'
+ test('renders side-by-side selected', async () => {
+ userModel.setPreferences({
+ ...createPreferences(),
+ diff_view: DiffViewMode.SIDE_BY_SIDE,
+ });
+ await waitUntilObserved(
+ browserModel.diffViewMode$,
+ mode => mode === DiffViewMode.SIDE_BY_SIDE
);
- assert.equal(element._computeSideBySideSelected(DiffViewMode.UNIFIED), '');
- assert.equal(
- element._computeUnifiedSelected(DiffViewMode.UNIFIED),
- 'selected'
- );
- assert.equal(
- element._computeUnifiedSelected(DiffViewMode.SIDE_BY_SIDE),
- ''
- );
+
+ expect(element).shadowDom.to.equal(/* HTML */ `
+ <gr-tooltip-content has-tooltip="" title="Side-by-side diff">
+ <gr-button
+ id="sideBySideBtn"
+ link=""
+ class="selected"
+ aria-disabled="false"
+ aria-pressed="true"
+ role="button"
+ tabindex="0"
+ >
+ <iron-icon icon="gr-icons:side-by-side"></iron-icon>
+ </gr-button>
+ </gr-tooltip-content>
+ <gr-tooltip-content has-tooltip title="Unified diff">
+ <gr-button
+ id="unifiedBtn"
+ link=""
+ role="button"
+ aria-disabled="false"
+ aria-pressed="false"
+ tabindex="0"
+ >
+ <iron-icon icon="gr-icons:unified"></iron-icon>
+ </gr-button>
+ </gr-tooltip-content>
+ `);
});
- test('setMode', () => {
- element.getBrowserModel().setScreenWidth(0);
+ test('renders unified selected', async () => {
+ userModel.setPreferences({
+ ...createPreferences(),
+ diff_view: DiffViewMode.UNIFIED,
+ });
+ await waitUntilObserved(
+ browserModel.diffViewMode$,
+ mode => mode === DiffViewMode.UNIFIED
+ );
+
+ expect(element).shadowDom.to.equal(/* HTML */ `
+ <gr-tooltip-content has-tooltip="" title="Side-by-side diff">
+ <gr-button
+ id="sideBySideBtn"
+ link=""
+ class=""
+ aria-disabled="false"
+ aria-pressed="false"
+ role="button"
+ tabindex="0"
+ >
+ <iron-icon icon="gr-icons:side-by-side"></iron-icon>
+ </gr-button>
+ </gr-tooltip-content>
+ <gr-tooltip-content has-tooltip title="Unified diff">
+ <gr-button
+ id="unifiedBtn"
+ link=""
+ class="selected"
+ role="button"
+ aria-disabled="false"
+ aria-pressed="true"
+ tabindex="0"
+ >
+ <iron-icon icon="gr-icons:unified"></iron-icon>
+ </gr-button>
+ </gr-tooltip-content>
+ `);
+ });
+
+ test('set mode', async () => {
+ browserModel.setScreenWidth(0);
const saveStub = stubUsers('updatePreferences');
- flush();
// Setting the mode initially does not save prefs.
element.saveOnChange = true;
- element.setMode(DiffViewMode.SIDE_BY_SIDE);
+ queryAndAssert<GrButton>(element, 'gr-button#sideBySideBtn').click();
+ await element.updateComplete;
+
assert.isFalse(saveStub.called);
// Setting the mode to itself does not save prefs.
- element.setMode(DiffViewMode.SIDE_BY_SIDE);
+ queryAndAssert<GrButton>(element, 'gr-button#sideBySideBtn').click();
+ await element.updateComplete;
+
assert.isFalse(saveStub.called);
// Setting the mode to something else does not save prefs if saveOnChange
// is false.
element.saveOnChange = false;
- element.setMode(DiffViewMode.UNIFIED);
+ queryAndAssert<GrButton>(element, 'gr-button#unifiedBtn').click();
+ await element.updateComplete;
+
assert.isFalse(saveStub.called);
// Setting the mode to something else does not save prefs if saveOnChange
// is false.
element.saveOnChange = true;
- element.setMode(DiffViewMode.SIDE_BY_SIDE);
+ queryAndAssert<GrButton>(element, 'gr-button#sideBySideBtn').click();
+ await element.updateComplete;
+
assert.isTrue(saveStub.calledOnce);
});
});
diff --git a/polygerrit-ui/app/embed/diff/gr-diff-selection/gr-diff-selection.ts b/polygerrit-ui/app/embed/diff/gr-diff-selection/gr-diff-selection.ts
index 2665ef0..22af7e3 100644
--- a/polygerrit-ui/app/embed/diff/gr-diff-selection/gr-diff-selection.ts
+++ b/polygerrit-ui/app/embed/diff/gr-diff-selection/gr-diff-selection.ts
@@ -23,7 +23,11 @@
normalize,
NormalizedRange,
} from '../gr-diff-highlight/gr-range-normalizer';
-import {descendedFromClass, querySelectorAll} from '../../../utils/dom-util';
+import {
+ descendedFromClass,
+ isElementTarget,
+ querySelectorAll,
+} from '../../../utils/dom-util';
import {customElement, property, observe} from '@polymer/decorators';
import {DiffInfo} from '../../../types/diff';
import {Side} from '../../../constants/constants';
@@ -109,8 +113,9 @@
}
_handleDown(e: Event) {
- const target = e.target;
- if (!(target instanceof Element)) return;
+ const target = e.composedPath()[0];
+ if (!isElementTarget(target)) return;
+
// Handle the down event on comment thread in Polymer 2
const handled = this._handleDownOnRangeComment(target);
if (handled) return;
diff --git a/polygerrit-ui/app/embed/diff/gr-diff/gr-diff-line.ts b/polygerrit-ui/app/embed/diff/gr-diff/gr-diff-line.ts
index 2927101..8593e1b 100644
--- a/polygerrit-ui/app/embed/diff/gr-diff/gr-diff-line.ts
+++ b/polygerrit-ui/app/embed/diff/gr-diff/gr-diff-line.ts
@@ -19,6 +19,7 @@
GrDiffLine as GrDiffLineApi,
GrDiffLineType,
LineNumber,
+ Side,
} from '../../../api/diff';
export {GrDiffLineType, LineNumber};
@@ -38,6 +39,10 @@
text = '';
+ lineNumber(side: Side) {
+ return side === Side.LEFT ? this.beforeNumber : this.afterNumber;
+ }
+
// TODO(TS): remove this properties
static readonly Type = GrDiffLineType;
diff --git a/polygerrit-ui/app/embed/diff/gr-diff/gr-diff-utils.ts b/polygerrit-ui/app/embed/diff/gr-diff/gr-diff-utils.ts
index 182d48e..a12867f 100644
--- a/polygerrit-ui/app/embed/diff/gr-diff/gr-diff-utils.ts
+++ b/polygerrit-ui/app/embed/diff/gr-diff/gr-diff-utils.ts
@@ -45,12 +45,20 @@
* Graphemes: http://unicode.org/reports/tr29/#Grapheme_Cluster_Boundaries
* A proposed JS API: https://github.com/tc39/proposal-intl-segmenter
*/
-const REGEX_TAB_OR_SURROGATE_PAIR = /\t|[\uD800-\uDBFF][\uDC00-\uDFFF]/;
+export const REGEX_TAB_OR_SURROGATE_PAIR = /\t|[\uD800-\uDBFF][\uDC00-\uDFFF]/;
// If any line of the diff is more than the character limit, then disable
// syntax highlighting for the entire file.
export const SYNTAX_MAX_LINE_LENGTH = 500;
+export function countLines(diff?: DiffInfo, side?: Side) {
+ if (!diff?.content || !side) return 0;
+ return diff.content.reduce((sum, chunk) => {
+ const sideChunk = side === Side.LEFT ? chunk.a : chunk.b;
+ return sum + (sideChunk?.length ?? chunk.ab?.length ?? chunk.skip ?? 0);
+ }, 0);
+}
+
export function getResponsiveMode(
prefs: DiffPreferencesInfo,
renderPrefs?: RenderPreferences
@@ -65,7 +73,7 @@
return 'NONE';
}
-export function isResponsive(responsiveMode: DiffResponsiveMode) {
+export function isResponsive(responsiveMode?: DiffResponsiveMode) {
return (
responsiveMode === 'FULL_RESPONSIVE' || responsiveMode === 'SHRINK_ONLY'
);
@@ -116,7 +124,12 @@
return null;
}
}
- node = node.previousSibling ?? node.parentElement ?? undefined;
+ node =
+ (node as Element).assignedSlot ??
+ (node as ShadowRoot).host ??
+ node.previousSibling ??
+ node.parentNode ??
+ undefined;
}
return null;
}
@@ -195,6 +208,19 @@
}
/**
+ * Simple helper method for creating element classes in the context of
+ * gr-diff.
+ *
+ * We are adding 'style-scope', 'gr-diff' classes for compatibility with
+ * Shady DOM. TODO: Is that still required??
+ *
+ * Otherwise this is just a super simple convenience function.
+ */
+export function diffClasses(...additionalClasses: string[]) {
+ return ['style-scope', 'gr-diff', ...additionalClasses].join(' ');
+}
+
+/**
* Simple helper method for creating elements in the context of gr-diff.
*
* We are adding 'style-scope', 'gr-diff' classes for compatibility with
@@ -255,6 +281,8 @@
}
/**
+ * Deprecated: Lit based rendering uses the textToPieces() function above.
+ *
* Returns a 'div' element containing the supplied |text| as its innerText,
* with '\t' characters expanded to a width determined by |tabSize|, and the
* text wrapped at column |lineLimit|, which may be Infinity if no wrapping is
diff --git a/polygerrit-ui/app/embed/diff/gr-diff/gr-diff-utils_test.ts b/polygerrit-ui/app/embed/diff/gr-diff/gr-diff-utils_test.ts
index 600913e..793b0ef 100644
--- a/polygerrit-ui/app/embed/diff/gr-diff/gr-diff-utils_test.ts
+++ b/polygerrit-ui/app/embed/diff/gr-diff/gr-diff-utils_test.ts
@@ -4,150 +4,171 @@
* SPDX-License-Identifier: Apache-2.0
*/
import '../../../test/common-test-setup-karma';
-import {createElementDiff, formatText, createTabWrapper} from './gr-diff-utils';
+import {
+ createElementDiff,
+ formatText,
+ createTabWrapper,
+ diffClasses,
+} from './gr-diff-utils';
const LINE_BREAK_HTML = '<span class="style-scope gr-diff br"></span>';
suite('gr-diff-utils tests', () => {
- test('createElementDiff classStr applies all classes', () => {
- const node = createElementDiff('div', 'test classes');
- assert.isTrue(node.classList.contains('gr-diff'));
- assert.isTrue(node.classList.contains('test'));
- assert.isTrue(node.classList.contains('classes'));
- });
+ suite('legacy rendering', () => {
+ test('createElementDiff classStr applies all classes', () => {
+ const node = createElementDiff('div', 'test classes');
+ assert.isTrue(node.classList.contains('gr-diff'));
+ assert.isTrue(node.classList.contains('test'));
+ assert.isTrue(node.classList.contains('classes'));
+ });
- test('formatText newlines 1', () => {
- let text = 'abcdef';
+ test('formatText newlines 1', () => {
+ let text = 'abcdef';
- assert.equal(formatText(text, 'NONE', 4, 10).innerHTML, text);
- text = 'a'.repeat(20);
- assert.equal(
- formatText(text, 'NONE', 4, 10).innerHTML,
- 'a'.repeat(10) + LINE_BREAK_HTML + 'a'.repeat(10)
- );
- });
+ assert.equal(formatText(text, 'NONE', 4, 10).innerHTML, text);
+ text = 'a'.repeat(20);
+ assert.equal(
+ formatText(text, 'NONE', 4, 10).innerHTML,
+ 'a'.repeat(10) + LINE_BREAK_HTML + 'a'.repeat(10)
+ );
+ });
- test('formatText newlines 2', () => {
- const text = '<span class="thumbsup">👍</span>';
- assert.equal(
- formatText(text, 'NONE', 4, 10).innerHTML,
- '<span clas' +
- LINE_BREAK_HTML +
- 's="thumbsu' +
- LINE_BREAK_HTML +
- 'p">👍</span' +
- LINE_BREAK_HTML +
- '>'
- );
- });
+ test('formatText newlines 2', () => {
+ const text = '<span class="thumbsup">👍</span>';
+ assert.equal(
+ formatText(text, 'NONE', 4, 10).innerHTML,
+ '<span clas' +
+ LINE_BREAK_HTML +
+ 's="thumbsu' +
+ LINE_BREAK_HTML +
+ 'p">👍</span' +
+ LINE_BREAK_HTML +
+ '>'
+ );
+ });
- test('formatText newlines 3', () => {
- const text = '01234\t56789';
- assert.equal(
- formatText(text, 'NONE', 4, 10).innerHTML,
- '01234' + createTabWrapper(3).outerHTML + '56' + LINE_BREAK_HTML + '789'
- );
- });
+ test('formatText newlines 3', () => {
+ const text = '01234\t56789';
+ assert.equal(
+ formatText(text, 'NONE', 4, 10).innerHTML,
+ '01234' + createTabWrapper(3).outerHTML + '56' + LINE_BREAK_HTML + '789'
+ );
+ });
- test('formatText newlines 4', () => {
- const text = '👍'.repeat(58);
- assert.equal(
- formatText(text, 'NONE', 4, 20).innerHTML,
- '👍'.repeat(20) +
- LINE_BREAK_HTML +
+ test('formatText newlines 4', () => {
+ const text = '👍'.repeat(58);
+ assert.equal(
+ formatText(text, 'NONE', 4, 20).innerHTML,
'👍'.repeat(20) +
- LINE_BREAK_HTML +
- '👍'.repeat(18)
- );
- });
+ LINE_BREAK_HTML +
+ '👍'.repeat(20) +
+ LINE_BREAK_HTML +
+ '👍'.repeat(18)
+ );
+ });
- test('tab wrapper style', () => {
- const pattern = new RegExp(
- '^<span class="style-scope gr-diff tab" ' +
- 'style="((?:-moz-)?tab-size: (\\d+);.?)+">\\t<\\/span>$'
- );
-
- for (const size of [1, 3, 8, 55]) {
- const html = createTabWrapper(size).outerHTML;
- expect(html).to.match(pattern);
- assert.equal(html.match(pattern)?.[2], size.toString());
- }
- });
-
- test('tab wrapper insertion', () => {
- const html = 'abc\tdef';
- const tabSize = 8;
- const wrapper = createTabWrapper(tabSize - 3);
- assert.ok(wrapper);
- assert.equal(wrapper.innerText, '\t');
- assert.equal(
- formatText(html, 'NONE', tabSize, Infinity).innerHTML,
- 'abc' + wrapper.outerHTML + 'def'
- );
- });
-
- test('escaping HTML', () => {
- let input = '<script>alert("XSS");<' + '/script>';
- let expected = '<script>alert("XSS");</script>';
-
- let result = formatText(
- input,
- 'NONE',
- 1,
- Number.POSITIVE_INFINITY
- ).innerHTML;
- assert.equal(result, expected);
-
- input = '& < > " \' / `';
- expected = '& < > " \' / `';
- result = formatText(input, 'NONE', 1, Number.POSITIVE_INFINITY).innerHTML;
- assert.equal(result, expected);
- });
-
- test('text length with tabs and unicode', () => {
- function expectTextLength(text: string, tabSize: number, expected: number) {
- // Formatting to |expected| columns should not introduce line breaks.
- const result = formatText(text, 'NONE', tabSize, expected);
- assert.isNotOk(
- result.querySelector('.contentText > .br'),
- ' Expected the result of: \n' +
- ` _formatText(${text}', 'NONE', ${tabSize}, ${expected})\n` +
- ' to not contain a br. But the actual result HTML was:\n' +
- ` '${result.innerHTML}'\nwhereupon`
+ test('tab wrapper style', () => {
+ const pattern = new RegExp(
+ '^<span class="style-scope gr-diff tab" ' +
+ 'style="((?:-moz-)?tab-size: (\\d+);.?)+">\\t<\\/span>$'
);
- // Increasing the line limit should produce the same markup.
- assert.equal(
- formatText(text, 'NONE', tabSize, Infinity).innerHTML,
- result.innerHTML
- );
- assert.equal(
- formatText(text, 'NONE', tabSize, expected + 1).innerHTML,
- result.innerHTML
- );
-
- // Decreasing the line limit should introduce line breaks.
- if (expected > 0) {
- const tooSmall = formatText(text, 'NONE', tabSize, expected - 1);
- assert.isOk(
- tooSmall.querySelector('.contentText > .br'),
- ' Expected the result of: \n' +
- ` _formatText(${text}', ${tabSize}, ${expected - 1})\n` +
- ' to contain a br. But the actual result HTML was:\n' +
- ` '${tooSmall.innerHTML}'\nwhereupon`
- );
+ for (const size of [1, 3, 8, 55]) {
+ const html = createTabWrapper(size).outerHTML;
+ expect(html).to.match(pattern);
+ assert.equal(html.match(pattern)?.[2], size.toString());
}
- }
- expectTextLength('12345', 4, 5);
- expectTextLength('\t\t12', 4, 10);
- expectTextLength('abc💢123', 4, 7);
- expectTextLength('abc\t', 8, 8);
- expectTextLength('abc\t\t', 10, 20);
- expectTextLength('', 10, 0);
- // 17 Thai combining chars.
- expectTextLength('ก้้้้้้้้้้้้้้้้', 4, 17);
- expectTextLength('abc\tde', 10, 12);
- expectTextLength('abc\tde\t', 10, 20);
- expectTextLength('\t\t\t\t\t', 20, 100);
+ });
+
+ test('tab wrapper insertion', () => {
+ const html = 'abc\tdef';
+ const tabSize = 8;
+ const wrapper = createTabWrapper(tabSize - 3);
+ assert.ok(wrapper);
+ assert.equal(wrapper.innerText, '\t');
+ assert.equal(
+ formatText(html, 'NONE', tabSize, Infinity).innerHTML,
+ 'abc' + wrapper.outerHTML + 'def'
+ );
+ });
+
+ test('escaping HTML', () => {
+ let input = '<script>alert("XSS");<' + '/script>';
+ let expected = '<script>alert("XSS");</script>';
+
+ let result = formatText(
+ input,
+ 'NONE',
+ 1,
+ Number.POSITIVE_INFINITY
+ ).innerHTML;
+ assert.equal(result, expected);
+
+ input = '& < > " \' / `';
+ expected = '& < > " \' / `';
+ result = formatText(input, 'NONE', 1, Number.POSITIVE_INFINITY).innerHTML;
+ assert.equal(result, expected);
+ });
+
+ test('text length with tabs and unicode', () => {
+ function expectTextLength(
+ text: string,
+ tabSize: number,
+ expected: number
+ ) {
+ // Formatting to |expected| columns should not introduce line breaks.
+ const result = formatText(text, 'NONE', tabSize, expected);
+ assert.isNotOk(
+ result.querySelector('.contentText > .br'),
+ ' Expected the result of: \n' +
+ ` _formatText(${text}', 'NONE', ${tabSize}, ${expected})\n` +
+ ' to not contain a br. But the actual result HTML was:\n' +
+ ` '${result.innerHTML}'\nwhereupon`
+ );
+
+ // Increasing the line limit should produce the same markup.
+ assert.equal(
+ formatText(text, 'NONE', tabSize, Infinity).innerHTML,
+ result.innerHTML
+ );
+ assert.equal(
+ formatText(text, 'NONE', tabSize, expected + 1).innerHTML,
+ result.innerHTML
+ );
+
+ // Decreasing the line limit should introduce line breaks.
+ if (expected > 0) {
+ const tooSmall = formatText(text, 'NONE', tabSize, expected - 1);
+ assert.isOk(
+ tooSmall.querySelector('.contentText > .br'),
+ ' Expected the result of: \n' +
+ ` _formatText(${text}', ${tabSize}, ${expected - 1})\n` +
+ ' to contain a br. But the actual result HTML was:\n' +
+ ` '${tooSmall.innerHTML}'\nwhereupon`
+ );
+ }
+ }
+ expectTextLength('12345', 4, 5);
+ expectTextLength('\t\t12', 4, 10);
+ expectTextLength('abc💢123', 4, 7);
+ expectTextLength('abc\t', 8, 8);
+ expectTextLength('abc\t\t', 10, 20);
+ expectTextLength('', 10, 0);
+ // 17 Thai combining chars.
+ expectTextLength('ก้้้้้้้้้้้้้้้้', 4, 17);
+ expectTextLength('abc\tde', 10, 12);
+ expectTextLength('abc\tde\t', 10, 20);
+ expectTextLength('\t\t\t\t\t', 20, 100);
+ });
+ });
+
+ suite('lit rendering', () => {
+ test('diffClasses', () => {
+ const c = diffClasses('div', 'test classes').split(' ');
+ assert.include(c, 'gr-diff');
+ assert.include(c, 'style-scope');
+ assert.include(c, 'test');
+ assert.include(c, 'classes');
+ });
});
});
diff --git a/polygerrit-ui/app/embed/diff/gr-diff/gr-diff.ts b/polygerrit-ui/app/embed/diff/gr-diff/gr-diff.ts
index f504614..f7e40f9 100644
--- a/polygerrit-ui/app/embed/diff/gr-diff/gr-diff.ts
+++ b/polygerrit-ui/app/embed/diff/gr-diff/gr-diff.ts
@@ -24,7 +24,7 @@
import '../gr-ranged-comment-themes/gr-ranged-comment-theme';
import '../gr-ranged-comment-hint/gr-ranged-comment-hint';
import {PolymerElement} from '@polymer/polymer/polymer-element';
-import {dom, EventApi} from '@polymer/polymer/lib/legacy/polymer.dom';
+import {dom} from '@polymer/polymer/lib/legacy/polymer.dom';
import {htmlTemplate} from './gr-diff_html';
import {LineNumber} from './gr-diff-line';
import {
@@ -77,7 +77,7 @@
GrDiff as GrDiffApi,
DisplayLine,
} from '../../../api/diff';
-import {isSafari, toggleClass} from '../../../utils/dom-util';
+import {isElementTarget, isSafari, toggleClass} from '../../../utils/dom-util';
import {assertIsDefined} from '../../../utils/common-util';
import {debounce, DelayedTask} from '../../../utils/async-util';
@@ -485,10 +485,16 @@
getCursorStops(): Array<HTMLElement | AbortStop> {
if (this.hidden && this.noAutoRender) return [];
+ // Get rendered stops.
+ const stops: Array<HTMLElement | AbortStop> =
+ this.$.diffBuilder.getLineNumberRows();
+
+ // If we are still loading this diff, abort after the rendered stops to
+ // avoid skipping over to e.g. the next file.
if (this.loading) {
- return [new AbortStop()];
+ stops.push(new AbortStop());
}
- return this.$.diffBuilder.getLineNumberRows();
+ return stops;
}
isRangeSelected() {
@@ -524,7 +530,8 @@
}
_handleTap(e: CustomEvent) {
- const el = (dom(e) as EventApi).localTarget as Element;
+ const el = e.composedPath()[0];
+ if (!isElementTarget(el)) return;
if (
el.getAttribute('data-value') !== 'LOST' &&
@@ -752,6 +759,10 @@
// border-right in ".section" css definition (in gr-diff_html.ts)
const sectionRightBorder = '1px';
+ // each sign col has 1ch width.
+ const signColsWidth =
+ sideBySide && renderPrefs?.show_sign_col ? '2ch' : '0ch';
+
// As some of these calculations are done using 'ch' we end up
// having <1px difference between ideal and calculated size for each side
// leading to lines using the max columns (e.g. 80) to wrap (decided
@@ -765,7 +776,7 @@
const dontWrapCorrection = '2px';
stylesToUpdate[
'--diff-max-width'
- ] = `calc(${contentWidth} + ${lineNumberWidth} + ${sectionRightBorder} + ${dontWrapCorrection})`;
+ ] = `calc(${contentWidth} + ${lineNumberWidth} + ${signColsWidth} + ${sectionRightBorder} + ${dontWrapCorrection})`;
} else {
stylesToUpdate['--diff-max-width'] = 'none';
}
@@ -787,6 +798,9 @@
if (renderPrefs.hide_line_length_indicator) {
this.classList.add('hide-line-length-indicator');
}
+ if (renderPrefs.show_sign_col) {
+ this.classList.add('with-sign-col');
+ }
if (this.prefs) {
this._updatePreferenceStyles(this.prefs, renderPrefs);
}
diff --git a/polygerrit-ui/app/embed/diff/gr-diff/gr-diff_html.ts b/polygerrit-ui/app/embed/diff/gr-diff/gr-diff_html.ts
index 9ad615a..c2e5550 100644
--- a/polygerrit-ui/app/embed/diff/gr-diff/gr-diff_html.ts
+++ b/polygerrit-ui/app/embed/diff/gr-diff/gr-diff_html.ts
@@ -20,11 +20,11 @@
<style include="shared-styles">
/**
* This is used to hide all left side of the diff (e.g. diffs besides comments
- * in the change log). Since we want to remove the first 3 cells consistently
+ * in the change log). Since we want to remove the first 4 cells consistently
* in all rows except context buttons (.dividerRow).
*/
- :host(.no-left) .sideBySide colgroup col:nth-child(-n + 3),
- :host(.no-left) .sideBySide tr:not(.dividerRow) td:nth-child(-n + 3) {
+ :host(.no-left) .sideBySide colgroup col:nth-child(-n + 4),
+ :host(.no-left) .sideBySide tr:not(.dividerRow) td:nth-child(-n + 4) {
display: none;
}
:host(.disable-context-control-buttons) {
@@ -70,8 +70,8 @@
}
/* Provides the option to add side borders (left and right) to the line number column. */
- td.left,
- td.right,
+ td.lineNum,
+ td.blankLineNum,
td.moveControlsLineNumCol,
td.contextLineNum {
box-shadow: var(--line-number-box-shadow, unset);
@@ -200,6 +200,14 @@
}
/**
+ * the outline for the sign cell should be always be contiguous top/bottom.
+ */
+ .target-row.target-side-left td.left.sign::before,
+ .target-row.target-side-right td.right.sign::before {
+ border-width: 1px 0;
+ }
+
+ /**
* For side-by-side we need to select the correct line number to "visually close"
* the outline.
*/
@@ -286,6 +294,14 @@
.canComment .lineNumButton {
cursor: pointer;
}
+ .sign {
+ min-width: 1ch;
+ width: 1ch;
+ background-color: var(--view-background-color);
+ }
+ .sign.blank {
+ background-color: var(--diff-blank-background-color);
+ }
.content {
/* Set min width since setting width on table cells still
allows them to shrink. Do not set max width because
@@ -296,22 +312,30 @@
.content.add .contentText .intraline,
/* If there are no intraline info, consider everything changed */
.content.add.no-intraline-info .contentText,
+ .sign.add.no-intraline-info,
.delta.total .content.add .contentText {
background-color: var(--dark-add-highlight-color);
}
- .content.add .contentText {
+ .content.add .contentText,
+ .sign.add {
background-color: var(--light-add-highlight-color);
}
.content.remove .contentText .intraline,
/* If there are no intraline info, consider everything changed */
.content.remove.no-intraline-info .contentText,
- .delta.total .content.remove .contentText {
+ .delta.total .content.remove .contentText,
+ .sign.remove.no-intraline-info {
background-color: var(--dark-remove-highlight-color);
}
- .content.remove .contentText {
+ .content.remove .contentText,
+ .sign.remove {
background-color: var(--light-remove-highlight-color);
}
+ .ignoredWhitespaceOnly .sign.no-intraline-info {
+ background-color: var(--view-background-color);
+ }
+
/* dueToRebase */
.dueToRebase .content.add .contentText .intraline,
.delta.total.dueToRebase .content.add .contentText {
@@ -329,14 +353,18 @@
}
/* dueToMove */
+ .dueToMove .sign.add,
.dueToMove .content.add .contentText,
+ .dueToMove .moveControls.movedIn .sign.right,
.dueToMove .moveControls.movedIn .moveHeader,
.delta.total.dueToMove .content.add .contentText {
background-color: var(--diff-moved-in-background);
}
+ .dueToMove .sign.remove,
.dueToMove .content.remove .contentText,
.dueToMove .moveControls.movedOut .moveHeader,
+ .dueToMove .moveControls.movedOut .sign.left,
.delta.total.dueToMove .content.remove .contentText {
background-color: var(--diff-moved-out-background);
}
@@ -495,6 +523,21 @@
padding: 0 var(--spacing-s) 0 var(--spacing-m);
color: var(--blue-700);
}
+
+ col.sign,
+ td.sign {
+ display: none;
+ }
+
+ /**
+ * Sign column should only be shown in high-contrast mode.
+ */
+ :host(.with-sign-col) col.sign {
+ display: table-column;
+ }
+ :host(.with-sign-col) td.sign {
+ display: table-cell;
+ }
col.blame {
display: none;
}
@@ -627,6 +670,12 @@
.token-highlight {
background-color: var(--token-highlighting-color, #fffd54);
}
+
+ gr-diff-section,
+ gr-context-controls-section,
+ gr-diff-row {
+ display: contents;
+ }
</style>
<style include="gr-syntax-theme">
/* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
diff --git a/polygerrit-ui/app/embed/diff/gr-diff/gr-diff_test.js b/polygerrit-ui/app/embed/diff/gr-diff/gr-diff_test.js
index 2c84ce3..714005e 100644
--- a/polygerrit-ui/app/embed/diff/gr-diff/gr-diff_test.js
+++ b/polygerrit-ui/app/embed/diff/gr-diff/gr-diff_test.js
@@ -25,6 +25,7 @@
import '@polymer/paper-button/paper-button.js';
import {Side} from '../../../api/diff.js';
import {mockPromise, stubRestApi} from '../../../test/test-utils.js';
+import {AbortStop} from '../../../api/core.js';
const basicFixture = fixtureFromElement('gr-diff');
@@ -124,14 +125,14 @@
element.viewMode = 'SIDE_BY_SIDE';
flush();
assert.equal(getComputedStyleValue('--diff-max-width', element),
- 'calc(2 * 80ch + 2 * 48px + 1px + 2px)');
+ 'calc(2 * 80ch + 2 * 48px + 0ch + 1px + 2px)');
});
test('max-width considers one content column in unified', () => {
element.viewMode = 'UNIFIED_DIFF';
flush();
assert.equal(getComputedStyleValue('--diff-max-width', element),
- 'calc(1 * 80ch + 2 * 48px + 1px + 2px)');
+ 'calc(1 * 80ch + 2 * 48px + 0ch + 1px + 2px)');
});
test('max-width considers font-size', () => {
@@ -139,7 +140,14 @@
flush();
// Each line number column: 4 * 13 = 52px
assert.equal(getComputedStyleValue('--diff-max-width', element),
- 'calc(2 * 80ch + 2 * 52px + 1px + 2px)');
+ 'calc(2 * 80ch + 2 * 52px + 0ch + 1px + 2px)');
+ });
+
+ test('sign cols are considered if show_sign_col is true', () => {
+ element.renderPrefs = {...element.renderPrefs, show_sign_col: true};
+ flush();
+ assert.equal(getComputedStyleValue('--diff-max-width', element),
+ 'calc(2 * 80ch + 2 * 48px + 2ch + 1px + 2px)');
});
});
@@ -536,23 +544,38 @@
};
element._renderDiffTable();
- element._setLoading(false);
+
flush();
}
- test('getCursorStops returns [] when hidden and noAutoRender', () => {
+ test('returns [] when hidden and noAutoRender', () => {
element.noAutoRender = true;
setupDiff();
+ element._setLoading(false);
+ flush();
element.hidden = true;
assert.equal(element.getCursorStops().length, 0);
});
- test('getCursorStops', () => {
+ test('returns one stop per line and one for the file row', () => {
setupDiff();
+ element._setLoading(false);
+ flush();
const ROWS = 48;
const FILE_ROW = 1;
assert.equal(element.getCursorStops().length, ROWS + FILE_ROW);
});
+
+ test('returns an additional AbortStop when still loading', () => {
+ setupDiff();
+ element._setLoading(true);
+ flush();
+ const ROWS = 48;
+ const FILE_ROW = 1;
+ const actual = element.getCursorStops();
+ assert.equal(actual.length, ROWS + FILE_ROW + 1);
+ assert.isTrue(actual[actual.length -1] instanceof AbortStop);
+ });
});
test('adds .hiddenscroll', () => {
diff --git a/polygerrit-ui/app/embed/diff/gr-ranged-comment-layer/gr-ranged-comment-layer.ts b/polygerrit-ui/app/embed/diff/gr-ranged-comment-layer/gr-ranged-comment-layer.ts
index 9cab977..6c8a5e9 100644
--- a/polygerrit-ui/app/embed/diff/gr-ranged-comment-layer/gr-ranged-comment-layer.ts
+++ b/polygerrit-ui/app/embed/diff/gr-ranged-comment-layer/gr-ranged-comment-layer.ts
@@ -1,30 +1,12 @@
/**
* @license
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
+ * Copyright 2016 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
*/
import {GrAnnotation} from '../gr-diff-highlight/gr-annotation';
-import {PolymerElement} from '@polymer/polymer/polymer-element';
-import {htmlTemplate} from './gr-ranged-comment-layer_html';
import {GrDiffLine, GrDiffLineType} from '../gr-diff/gr-diff-line';
import {strToClassName} from '../../../utils/dom-util';
-import {customElement, property, observe} from '@polymer/decorators';
import {Side} from '../../../constants/constants';
-import {
- PolymerDeepPropertyChange,
- PolymerSpliceChange,
-} from '@polymer/polymer/interfaces';
import {CommentRange} from '../../../types/common';
import {DiffLayer, DiffLayerListener} from '../../../types/types';
import {isLongCommentRange} from '../gr-diff/gr-diff-utils';
@@ -39,7 +21,18 @@
side: Side;
range: CommentRange;
hovering: boolean;
- rootId: string;
+ // New drafts don't have a rootId.
+ rootId?: string;
+}
+
+/** Can be used for array functions like `some()`. */
+function equals(a: CommentRangeLayer) {
+ return (b: CommentRangeLayer) => id(a) === id(b);
+}
+
+function id(r: CommentRangeLayer): string {
+ if (r.rootId) return r.rootId;
+ return `${r.side}-${r.range.start_line}-${r.range.start_character}-${r.range.end_line}-${r.range.end_character}`;
}
/**
@@ -49,8 +42,10 @@
interface CommentRangeLineLayer {
hovering: boolean;
longRange: boolean;
- rootId: string;
+ id: string;
+ // start char (0-based)
start: number;
+ // end char (0-based)
end: number;
}
@@ -62,40 +57,16 @@
[side in Side]: LinesMap;
};
-// Polymer 1 adds # before array's key, while Polymer 2 doesn't
-const HOVER_PATH_PATTERN = /^(commentRanges\.#?\d+)\.hovering$/;
-
const RANGE_BASE_ONLY = 'style-scope gr-diff range';
const RANGE_HIGHLIGHT = 'style-scope gr-diff range rangeHighlight';
const HOVER_HIGHLIGHT = 'style-scope gr-diff range rangeHoverHighlight';
-@customElement('gr-ranged-comment-layer')
-export class GrRangedCommentLayer extends PolymerElement implements DiffLayer {
- static get template() {
- return htmlTemplate;
- }
+export class GrRangedCommentLayer implements DiffLayer {
+ private knownRanges: CommentRangeLayer[] = [];
- /**
- * Fired when the range in a range comment was malformed and had to be
- * normalized.
- *
- * It's `detail` has a `lineNum` and `side` parameter.
- *
- * @event normalize-range
- */
+ private listeners: DiffLayerListener[] = [];
- @property({type: Array})
- commentRanges: CommentRangeLayer[] = [];
-
- @property({type: Array})
- _listeners: DiffLayerListener[] = [];
-
- @property({type: Object})
- _rangesMap: RangesMap = {left: {}, right: {}};
-
- get styleModuleName() {
- return 'gr-ranged-comment-styles';
- }
+ private rangesMap: RangesMap = {left: {}, right: {}};
/**
* Layer method to add annotations to a line.
@@ -107,16 +78,16 @@
if (
line.type === GrDiffLineType.REMOVE ||
(line.type === GrDiffLineType.BOTH &&
- el.getAttribute('data-side') !== 'right')
+ el.getAttribute('data-side') !== Side.RIGHT)
) {
- ranges = ranges.concat(this._getRangesForLine(line, Side.LEFT));
+ ranges = this.getRangesForLine(line, Side.LEFT);
}
if (
line.type === GrDiffLineType.ADD ||
(line.type === GrDiffLineType.BOTH &&
- el.getAttribute('data-side') !== 'left')
+ el.getAttribute('data-side') !== Side.LEFT)
) {
- ranges = ranges.concat(this._getRangesForLine(line, Side.RIGHT));
+ ranges = this.getRangesForLine(line, Side.RIGHT);
}
for (const range of ranges) {
@@ -128,7 +99,7 @@
? HOVER_HIGHLIGHT
: range.longRange
? RANGE_BASE_ONLY
- : RANGE_HIGHLIGHT) + ` ${strToClassName(range.rootId)}`
+ : RANGE_HIGHLIGHT) + ` ${strToClassName(range.id)}`
);
}
}
@@ -137,115 +108,71 @@
* Register a listener for layer updates.
*/
addListener(listener: DiffLayerListener) {
- this._listeners.push(listener);
+ this.listeners.push(listener);
}
removeListener(listener: DiffLayerListener) {
- this._listeners = this._listeners.filter(f => f !== listener);
+ this.listeners = this.listeners.filter(f => f !== listener);
}
/**
* Notify Layer listeners of changes to annotations.
*/
- _notifyUpdateRange(start: number, end: number, side: Side) {
- for (const listener of this._listeners) {
+ private notifyUpdateRange(start: number, end: number, side: Side) {
+ for (const listener of this.listeners) {
listener(start, end, side);
}
}
- /**
- * Handle change in the ranges by updating the ranges maps and by
- * emitting appropriate update notifications.
- */
- @observe('commentRanges.*')
- _handleCommentRangesChange(
- record: PolymerDeepPropertyChange<
- CommentRangeLayer[],
- PolymerSpliceChange<CommentRangeLayer[]>
- >
- ) {
- if (!record) return;
-
- // If the entire set of comments was changed.
- if (record.path === 'commentRanges') {
- const value = record.value as CommentRangeLayer[];
- this._rangesMap = {left: {}, right: {}};
- for (const {side, range, rootId, hovering} of value) {
- const longRange = isLongCommentRange(range);
- this._updateRangesMap({
- side,
- range,
- hovering,
- operation: (forLine, start, end, hovering) => {
- forLine.push({start, end, hovering, rootId, longRange});
- },
- });
- }
+ updateRanges(newRanges: CommentRangeLayer[]) {
+ for (const newRange of newRanges) {
+ if (this.knownRanges.some(equals(newRange))) continue;
+ this.addRange(newRange);
}
- // If the change only changed the `hovering` property of a comment.
- const match = record.path.match(HOVER_PATH_PATTERN);
- if (match) {
- // The #number indicates the key of that item in the array
- // not the index, especially in polymer 1.
- const {side, range, hovering, rootId} = this.get(match[1]);
-
- this._updateRangesMap({
- side,
- range,
- hovering,
- skipLayerUpdate: true,
- operation: (forLine, start, end, hovering) => {
- const index = forLine.findIndex(
- lineRange => lineRange.start === start && lineRange.end === end
- );
- forLine[index].hovering = hovering;
- forLine[index].rootId = rootId;
- },
- });
+ for (const knownRange of this.knownRanges) {
+ if (newRanges.some(equals(knownRange))) continue;
+ this.removeRange(knownRange);
}
- // If comments were spliced in or out.
- if (record.path === 'commentRanges.splices') {
- const value = record.value as PolymerSpliceChange<CommentRangeLayer[]>;
- for (const indexSplice of value.indexSplices) {
- const removed = indexSplice.removed;
- for (const {side, range, hovering, rootId} of removed) {
- this._updateRangesMap({
- side,
- range,
- hovering,
- operation: (forLine, start, end) => {
- const index = forLine.findIndex(
- lineRange =>
- lineRange.start === start &&
- lineRange.end === end &&
- rootId === lineRange.rootId
- );
- forLine.splice(index, 1);
- },
- });
- }
- const added = indexSplice.object.slice(
- indexSplice.index,
- indexSplice.index + indexSplice.addedCount
- );
- for (const {side, range, hovering, rootId} of added) {
- const longRange = isLongCommentRange(range);
- this._updateRangesMap({
- side,
- range,
- hovering,
- operation: (forLine, start, end, hovering) => {
- forLine.push({start, end, hovering, rootId, longRange});
- },
- });
- }
- }
- }
+ this.knownRanges = [...newRanges];
}
- _updateRangesMap(options: {
+ private addRange(commentRange: CommentRangeLayer) {
+ const {side, range, hovering} = commentRange;
+ const longRange = isLongCommentRange(range);
+ this.updateRangesMap({
+ side,
+ range,
+ hovering,
+ operation: (forLine, startChar, endChar, hovering) => {
+ forLine.push({
+ start: startChar,
+ end: endChar,
+ hovering,
+ id: id(commentRange),
+ longRange,
+ });
+ },
+ });
+ }
+
+ private removeRange(commentRange: CommentRangeLayer) {
+ const {side, range, hovering} = commentRange;
+ this.updateRangesMap({
+ side,
+ range,
+ hovering,
+ operation: forLine => {
+ const index = forLine.findIndex(
+ lineRange => id(commentRange) === lineRange.id
+ );
+ if (index > -1) forLine.splice(index, 1);
+ },
+ });
+ }
+
+ private updateRangesMap(options: {
side: Side;
range: CommentRange;
hovering: boolean;
@@ -255,25 +182,23 @@
end: number,
hovering: boolean
) => void;
- skipLayerUpdate?: boolean;
}) {
- const {side, range, hovering, operation, skipLayerUpdate} = options;
- const forSide = this._rangesMap[side] || (this._rangesMap[side] = {});
+ const {side, range, hovering, operation} = options;
+ const forSide = this.rangesMap[side] || (this.rangesMap[side] = {});
for (let line = range.start_line; line <= range.end_line; line++) {
const forLine = forSide[line] || (forSide[line] = []);
const start = line === range.start_line ? range.start_character : 0;
const end = line === range.end_line ? range.end_character : -1;
operation(forLine, start, end, hovering);
}
- if (!skipLayerUpdate) {
- this._notifyUpdateRange(range.start_line, range.end_line, side);
- }
+ this.notifyUpdateRange(range.start_line, range.end_line, side);
}
- _getRangesForLine(line: GrDiffLine, side: Side) {
+ // visible for testing
+ getRangesForLine(line: GrDiffLine, side: Side): CommentRangeLineLayer[] {
const lineNum = side === Side.LEFT ? line.beforeNumber : line.afterNumber;
- const ranges: CommentRangeLineLayer[] =
- this.get(['_rangesMap', side, lineNum]) || [];
+ if (lineNum === 'FILE' || lineNum === 'LOST') return [];
+ const ranges: CommentRangeLineLayer[] = this.rangesMap[side][lineNum] || [];
return (
ranges
.map(range => {
@@ -287,13 +212,6 @@
// @see Issue 5744
if (range.start >= range.end && range.start < line.text.length) {
range.end = line.text.length;
- this.dispatchEvent(
- new CustomEvent('normalize-range', {
- bubbles: true,
- composed: true,
- detail: {lineNum, side},
- })
- );
}
return range;
@@ -303,9 +221,3 @@
);
}
}
-
-declare global {
- interface HTMLElementTagNameMap {
- 'gr-ranged-comment-layer': GrRangedCommentLayer;
- }
-}
diff --git a/polygerrit-ui/app/embed/diff/gr-ranged-comment-layer/gr-ranged-comment-layer_html.ts b/polygerrit-ui/app/embed/diff/gr-ranged-comment-layer/gr-ranged-comment-layer_html.ts
deleted file mode 100644
index 1489006..0000000
--- a/polygerrit-ui/app/embed/diff/gr-ranged-comment-layer/gr-ranged-comment-layer_html.ts
+++ /dev/null
@@ -1,19 +0,0 @@
-/**
- * @license
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-import {html} from '@polymer/polymer/lib/utils/html-tag';
-
-export const htmlTemplate = html``;
diff --git a/polygerrit-ui/app/embed/diff/gr-ranged-comment-layer/gr-ranged-comment-layer_test.js b/polygerrit-ui/app/embed/diff/gr-ranged-comment-layer/gr-ranged-comment-layer_test.js
deleted file mode 100644
index 8279ab1..0000000
--- a/polygerrit-ui/app/embed/diff/gr-ranged-comment-layer/gr-ranged-comment-layer_test.js
+++ /dev/null
@@ -1,353 +0,0 @@
-/**
- * @license
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-import '../../../test/common-test-setup-karma.js';
-import '../gr-diff/gr-diff-line.js';
-import './gr-ranged-comment-layer.js';
-import {GrAnnotation} from '../gr-diff-highlight/gr-annotation.js';
-import {GrDiffLine, GrDiffLineType} from '../gr-diff/gr-diff-line.js';
-
-const basicFixture = fixtureFromElement('gr-ranged-comment-layer');
-
-suite('gr-ranged-comment-layer', () => {
- let element;
-
- setup(() => {
- const initialCommentRanges = [
- {
- side: 'left',
- range: {
- end_character: 9,
- end_line: 39,
- start_character: 6,
- start_line: 36,
- },
- rootId: 'a',
- },
- {
- side: 'right',
- range: {
- end_character: 22,
- end_line: 12,
- start_character: 10,
- start_line: 10,
- },
- rootId: 'b',
- },
- {
- side: 'right',
- range: {
- end_character: 15,
- end_line: 100,
- start_character: 5,
- start_line: 100,
- },
- rootId: 'c',
- },
- {
- side: 'right',
- range: {
- end_character: 2,
- end_line: 55,
- start_character: 32,
- start_line: 55,
- },
- rootId: 'd',
- },
- {
- side: 'right',
- range: {
- end_character: 1,
- end_line: 71,
- start_character: 1,
- start_line: 60,
- },
- },
- ];
-
- element = basicFixture.instantiate();
- element.commentRanges = initialCommentRanges;
- });
-
- suite('annotate', () => {
- let el;
- let line;
- let annotateElementStub;
- const lineNumberEl = document.createElement('td');
-
- setup(() => {
- annotateElementStub = sinon.stub(GrAnnotation, 'annotateElement');
- el = document.createElement('div');
- el.setAttribute('data-side', 'left');
- line = new GrDiffLine(GrDiffLineType.BOTH);
- line.text = 'Lorem ipsum dolor sit amet, consectetur adipiscing elit,';
- });
-
- test('type=Remove no-comment', () => {
- line.type = GrDiffLineType.REMOVE;
- line.beforeNumber = 40;
-
- element.annotate(el, lineNumberEl, line);
-
- assert.isFalse(annotateElementStub.called);
- });
-
- test('type=Remove has-comment', () => {
- line.type = GrDiffLineType.REMOVE;
- line.beforeNumber = 36;
- const expectedStart = 6;
- const expectedLength = line.text.length - expectedStart;
-
- element.annotate(el, lineNumberEl, line);
-
- assert.isTrue(annotateElementStub.called);
- const lastCall = annotateElementStub.lastCall;
- assert.equal(lastCall.args[0], el);
- assert.equal(lastCall.args[1], expectedStart);
- assert.equal(lastCall.args[2], expectedLength);
- assert.equal(
- lastCall.args[3],
- 'style-scope gr-diff range rangeHighlight generated_a'
- );
- });
-
- test('type=Remove has-comment hovering', () => {
- line.type = GrDiffLineType.REMOVE;
- line.beforeNumber = 36;
- element.set(['commentRanges', 0, 'hovering'], true);
-
- const expectedStart = 6;
- const expectedLength = line.text.length - expectedStart;
-
- element.annotate(el, lineNumberEl, line);
-
- assert.isTrue(annotateElementStub.called);
- const lastCall = annotateElementStub.lastCall;
- assert.equal(lastCall.args[0], el);
- assert.equal(lastCall.args[1], expectedStart);
- assert.equal(lastCall.args[2], expectedLength);
- assert.equal(
- lastCall.args[3],
- 'style-scope gr-diff range rangeHoverHighlight generated_a'
- );
- });
-
- test('type=Both has-comment', () => {
- line.type = GrDiffLineType.BOTH;
- line.beforeNumber = 36;
-
- const expectedStart = 6;
- const expectedLength = line.text.length - expectedStart;
-
- element.annotate(el, lineNumberEl, line);
-
- assert.isTrue(annotateElementStub.called);
- const lastCall = annotateElementStub.lastCall;
- assert.equal(lastCall.args[0], el);
- assert.equal(lastCall.args[1], expectedStart);
- assert.equal(lastCall.args[2], expectedLength);
- assert.equal(
- lastCall.args[3],
- 'style-scope gr-diff range rangeHighlight generated_a'
- );
- });
-
- test('type=Both has-comment off side', () => {
- line.type = GrDiffLineType.BOTH;
- line.beforeNumber = 36;
- el.setAttribute('data-side', 'right');
-
- element.annotate(el, lineNumberEl, line);
-
- assert.isFalse(annotateElementStub.called);
- });
-
- test('type=Add has-comment', () => {
- line.type = GrDiffLineType.ADD;
- line.afterNumber = 12;
- el.setAttribute('data-side', 'right');
-
- const expectedStart = 0;
- const expectedLength = 22;
-
- element.annotate(el, lineNumberEl, line);
-
- assert.isTrue(annotateElementStub.called);
- const lastCall = annotateElementStub.lastCall;
- assert.equal(lastCall.args[0], el);
- assert.equal(lastCall.args[1], expectedStart);
- assert.equal(lastCall.args[2], expectedLength);
- assert.equal(
- lastCall.args[3],
- 'style-scope gr-diff range rangeHighlight generated_b'
- );
- });
-
- test('long range comment', () => {
- line.type = GrDiffLineType.ADD;
- line.afterNumber = 65;
- el.setAttribute('data-side', 'right');
-
- element.annotate(el, lineNumberEl, line);
-
- assert.isTrue(annotateElementStub.called);
- assert.equal(
- annotateElementStub.lastCall.args[3],
- 'style-scope gr-diff range generated_'
- );
- });
- });
-
- test('_handleCommentRangesChange overwrite', () => {
- element.set('commentRanges', []);
-
- assert.equal(Object.keys(element._rangesMap.left).length, 0);
- assert.equal(Object.keys(element._rangesMap.right).length, 0);
- });
-
- test('_handleCommentRangesChange hovering', () => {
- const notifyStub = sinon.stub();
- element.addListener(notifyStub);
- const updateRangesMapSpy = sinon.spy(element, '_updateRangesMap');
-
- element.set(['commentRanges', 1, 'hovering'], true);
-
- // notify will be skipped for hovering
- assert.isFalse(notifyStub.called);
-
- assert.isTrue(updateRangesMapSpy.called);
- });
-
- test('_handleCommentRangesChange splice out', () => {
- const notifyStub = sinon.stub();
- element.addListener(notifyStub);
-
- element.splice('commentRanges', 1, 1);
-
- assert.isTrue(notifyStub.called);
- const lastCall = notifyStub.lastCall;
- assert.equal(lastCall.args[0], 10);
- assert.equal(lastCall.args[1], 12);
- assert.equal(lastCall.args[2], 'right');
- });
-
- test('_handleCommentRangesChange splice in', () => {
- const notifyStub = sinon.stub();
- element.addListener(notifyStub);
-
- element.splice('commentRanges', 1, 0, {
- side: 'left',
- range: {
- end_character: 15,
- end_line: 275,
- start_character: 5,
- start_line: 250,
- },
- });
-
- assert.isTrue(notifyStub.called);
- const lastCall = notifyStub.lastCall;
- assert.equal(lastCall.args[0], 250);
- assert.equal(lastCall.args[1], 275);
- assert.equal(lastCall.args[2], 'left');
- });
-
- test('_handleCommentRangesChange mixed actions', () => {
- const notifyStub = sinon.stub();
- element.addListener(notifyStub);
- const updateRangesMapSpy = sinon.spy(element, '_updateRangesMap');
-
- element.set(['commentRanges', 1, 'hovering'], true);
- assert.isTrue(updateRangesMapSpy.callCount === 1);
- element.splice('commentRanges', 1, 1);
- assert.isTrue(updateRangesMapSpy.callCount === 2);
- element.splice('commentRanges', 1, 1);
- assert.isTrue(updateRangesMapSpy.callCount === 3);
- element.splice('commentRanges', 1, 0, {
- side: 'left',
- range: {
- end_character: 15,
- end_line: 275,
- start_character: 5,
- start_line: 250,
- },
- });
- assert.isTrue(updateRangesMapSpy.callCount === 4);
- element.set(['commentRanges', 2, 'hovering'], true);
- assert.isTrue(updateRangesMapSpy.callCount === 5);
- });
-
- test('_computeCommentMap creates maps correctly', () => {
- // There is only one ranged comment on the left, but it spans ll.36-39.
- const leftKeys = [];
- for (let i = 36; i <= 39; i++) { leftKeys.push('' + i); }
- assert.deepEqual(Object.keys(element._rangesMap.left).sort(),
- leftKeys.sort());
-
- assert.equal(element._rangesMap.left[36].length, 1);
- assert.equal(element._rangesMap.left[36][0].start, 6);
- assert.equal(element._rangesMap.left[36][0].end, -1);
-
- assert.equal(element._rangesMap.left[37].length, 1);
- assert.equal(element._rangesMap.left[37][0].start, 0);
- assert.equal(element._rangesMap.left[37][0].end, -1);
-
- assert.equal(element._rangesMap.left[38].length, 1);
- assert.equal(element._rangesMap.left[38][0].start, 0);
- assert.equal(element._rangesMap.left[38][0].end, -1);
-
- assert.equal(element._rangesMap.left[39].length, 1);
- assert.equal(element._rangesMap.left[39][0].start, 0);
- assert.equal(element._rangesMap.left[39][0].end, 9);
-
- // The right has four ranged comments: 10-12, 55-55, 60-71, 100-100
- const rightKeys = [];
- for (let i = 10; i <= 12; i++) { rightKeys.push('' + i); }
- for (let i = 60; i <= 71; i++) { rightKeys.push('' + i); }
- rightKeys.push('55', '100');
- assert.deepEqual(Object.keys(element._rangesMap.right).sort(),
- rightKeys.sort());
-
- assert.equal(element._rangesMap.right[10].length, 1);
- assert.equal(element._rangesMap.right[10][0].start, 10);
- assert.equal(element._rangesMap.right[10][0].end, -1);
-
- assert.equal(element._rangesMap.right[11].length, 1);
- assert.equal(element._rangesMap.right[11][0].start, 0);
- assert.equal(element._rangesMap.right[11][0].end, -1);
-
- assert.equal(element._rangesMap.right[12].length, 1);
- assert.equal(element._rangesMap.right[12][0].start, 0);
- assert.equal(element._rangesMap.right[12][0].end, 22);
-
- assert.equal(element._rangesMap.right[100].length, 1);
- assert.equal(element._rangesMap.right[100][0].start, 5);
- assert.equal(element._rangesMap.right[100][0].end, 15);
- });
-
- test('_getRangesForLine normalizes invalid ranges', () => {
- const line = {
- afterNumber: 55,
- text: '_getRangesForLine normalizes invalid ranges',
- };
- const ranges = element._getRangesForLine(line, 'right');
- assert.equal(ranges.length, 1);
- const range = ranges[0];
- assert.isTrue(range.start < range.end, 'start and end are normalized');
- assert.equal(range.end, line.text.length);
- });
-});
diff --git a/polygerrit-ui/app/embed/diff/gr-ranged-comment-layer/gr-ranged-comment-layer_test.ts b/polygerrit-ui/app/embed/diff/gr-ranged-comment-layer/gr-ranged-comment-layer_test.ts
new file mode 100644
index 0000000..4e35645
--- /dev/null
+++ b/polygerrit-ui/app/embed/diff/gr-ranged-comment-layer/gr-ranged-comment-layer_test.ts
@@ -0,0 +1,301 @@
+/**
+ * @license
+ * Copyright 2016 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+import '../../../test/common-test-setup-karma';
+import '../gr-diff/gr-diff-line';
+import './gr-ranged-comment-layer';
+import {
+ CommentRangeLayer,
+ GrRangedCommentLayer,
+} from './gr-ranged-comment-layer';
+import {GrAnnotation} from '../gr-diff-highlight/gr-annotation';
+import {GrDiffLine, GrDiffLineType} from '../gr-diff/gr-diff-line';
+import {Side} from '../../../api/diff';
+import {SinonStub} from 'sinon';
+
+const rangeA: CommentRangeLayer = {
+ side: Side.LEFT,
+ range: {
+ end_character: 9,
+ end_line: 39,
+ start_character: 6,
+ start_line: 36,
+ },
+ rootId: 'a',
+ hovering: false,
+};
+
+const rangeB: CommentRangeLayer = {
+ side: Side.RIGHT,
+ range: {
+ end_character: 22,
+ end_line: 12,
+ start_character: 10,
+ start_line: 10,
+ },
+ rootId: 'b',
+ hovering: false,
+};
+
+const rangeC: CommentRangeLayer = {
+ side: Side.RIGHT,
+ range: {
+ end_character: 15,
+ end_line: 100,
+ start_character: 5,
+ start_line: 100,
+ },
+ hovering: false,
+};
+
+const rangeD: CommentRangeLayer = {
+ side: Side.RIGHT,
+ range: {
+ end_character: 2,
+ end_line: 55,
+ start_character: 32,
+ start_line: 55,
+ },
+ rootId: 'd',
+ hovering: false,
+};
+
+const rangeE: CommentRangeLayer = {
+ side: Side.RIGHT,
+ range: {
+ end_character: 1,
+ end_line: 71,
+ start_character: 1,
+ start_line: 60,
+ },
+ hovering: false,
+};
+
+suite('gr-ranged-comment-layer', () => {
+ let element: GrRangedCommentLayer;
+
+ setup(() => {
+ const initialCommentRanges: CommentRangeLayer[] = [
+ rangeA,
+ rangeB,
+ rangeC,
+ rangeD,
+ rangeE,
+ ];
+
+ element = new GrRangedCommentLayer();
+ element.updateRanges(initialCommentRanges);
+ });
+
+ suite('annotate', () => {
+ let el: HTMLDivElement;
+ let line: GrDiffLine;
+ let annotateElementStub: SinonStub;
+ const lineNumberEl = document.createElement('td');
+
+ function assertHasRange(
+ commentRange: CommentRangeLayer,
+ hasRange: boolean
+ ) {
+ assertHasRangeOn(
+ commentRange.side,
+ commentRange.range.start_line,
+ hasRange
+ );
+ }
+
+ function assertHasRangeOn(
+ side: Side,
+ lineNumber: number,
+ hasRange: boolean
+ ) {
+ line = new GrDiffLine(GrDiffLineType.BOTH);
+ if (side === Side.LEFT) line.beforeNumber = lineNumber;
+ if (side === Side.RIGHT) line.afterNumber = lineNumber;
+ el.setAttribute('data-side', side);
+
+ element.annotate(el, lineNumberEl, line);
+
+ assert.equal(annotateElementStub.called, hasRange);
+ annotateElementStub.reset();
+ }
+
+ setup(() => {
+ annotateElementStub = sinon.stub(GrAnnotation, 'annotateElement');
+ el = document.createElement('div');
+ el.setAttribute('data-side', Side.LEFT);
+ line = new GrDiffLine(GrDiffLineType.BOTH);
+ line.text = 'Lorem ipsum dolor sit amet, consectetur adipiscing elit,';
+ });
+
+ test('type=Remove no-comment', () => {
+ line = new GrDiffLine(GrDiffLineType.REMOVE);
+ line.beforeNumber = 40;
+
+ element.annotate(el, lineNumberEl, line);
+
+ assert.isFalse(annotateElementStub.called);
+ });
+
+ test('type=Remove has-comment', () => {
+ line = new GrDiffLine(GrDiffLineType.REMOVE);
+ line.beforeNumber = 36;
+ const expectedStart = 6;
+ const expectedLength = line.text.length - expectedStart;
+
+ element.annotate(el, lineNumberEl, line);
+
+ assert.isTrue(annotateElementStub.called);
+ const lastCall = annotateElementStub.lastCall;
+ assert.equal(lastCall.args[0], el);
+ assert.equal(lastCall.args[1], expectedStart);
+ assert.equal(lastCall.args[2], expectedLength);
+ assert.equal(
+ lastCall.args[3],
+ 'style-scope gr-diff range rangeHighlight generated_a'
+ );
+ });
+
+ test('type=Both has-comment', () => {
+ line = new GrDiffLine(GrDiffLineType.BOTH);
+ line.beforeNumber = 36;
+
+ const expectedStart = 6;
+ const expectedLength = line.text.length - expectedStart;
+
+ element.annotate(el, lineNumberEl, line);
+
+ assert.isTrue(annotateElementStub.called);
+ const lastCall = annotateElementStub.lastCall;
+ assert.equal(lastCall.args[0], el);
+ assert.equal(lastCall.args[1], expectedStart);
+ assert.equal(lastCall.args[2], expectedLength);
+ assert.equal(
+ lastCall.args[3],
+ 'style-scope gr-diff range rangeHighlight generated_a'
+ );
+ });
+
+ test('type=Both has-comment off side', () => {
+ line = new GrDiffLine(GrDiffLineType.BOTH);
+ line.beforeNumber = 36;
+ el.setAttribute('data-side', Side.RIGHT);
+
+ element.annotate(el, lineNumberEl, line);
+
+ assert.isFalse(annotateElementStub.called);
+ });
+
+ test('type=Add has-comment', () => {
+ line = new GrDiffLine(GrDiffLineType.ADD);
+ line.afterNumber = 12;
+ el.setAttribute('data-side', Side.RIGHT);
+
+ const expectedStart = 0;
+ const expectedLength = 22;
+
+ element.annotate(el, lineNumberEl, line);
+
+ assert.isTrue(annotateElementStub.called);
+ const lastCall = annotateElementStub.lastCall;
+ assert.equal(lastCall.args[0], el);
+ assert.equal(lastCall.args[1], expectedStart);
+ assert.equal(lastCall.args[2], expectedLength);
+ assert.equal(
+ lastCall.args[3],
+ 'style-scope gr-diff range rangeHighlight generated_b'
+ );
+ });
+
+ test('long range comment', () => {
+ line = new GrDiffLine(GrDiffLineType.ADD);
+ line.afterNumber = 65;
+ el.setAttribute('data-side', Side.RIGHT);
+
+ element.annotate(el, lineNumberEl, line);
+
+ assert.isTrue(annotateElementStub.called);
+ assert.equal(
+ annotateElementStub.lastCall.args[3],
+ 'style-scope gr-diff range generated_right-60-1-71-1'
+ );
+ });
+
+ test('updateRanges remove all', () => {
+ assertHasRange(rangeA, true);
+ assertHasRange(rangeB, true);
+ assertHasRange(rangeC, true);
+ assertHasRange(rangeD, true);
+ assertHasRange(rangeE, true);
+
+ element.updateRanges([]);
+
+ assertHasRange(rangeA, false);
+ assertHasRange(rangeB, false);
+ assertHasRange(rangeC, false);
+ assertHasRange(rangeD, false);
+ assertHasRange(rangeE, false);
+ });
+
+ test('updateRanges remove A and C', () => {
+ assertHasRange(rangeA, true);
+ assertHasRange(rangeB, true);
+ assertHasRange(rangeC, true);
+ assertHasRange(rangeD, true);
+ assertHasRange(rangeE, true);
+
+ element.updateRanges([rangeB, rangeD, rangeE]);
+
+ assertHasRange(rangeA, false);
+ assertHasRange(rangeB, true);
+ assertHasRange(rangeC, false);
+ assertHasRange(rangeD, true);
+ assertHasRange(rangeE, true);
+ });
+
+ test('updateRanges add B and D', () => {
+ element.updateRanges([]);
+
+ assertHasRange(rangeA, false);
+ assertHasRange(rangeB, false);
+ assertHasRange(rangeC, false);
+ assertHasRange(rangeD, false);
+ assertHasRange(rangeE, false);
+
+ element.updateRanges([rangeB, rangeD]);
+
+ assertHasRange(rangeA, false);
+ assertHasRange(rangeB, true);
+ assertHasRange(rangeC, false);
+ assertHasRange(rangeD, true);
+ assertHasRange(rangeE, false);
+ });
+
+ test('updateRanges add A, remove B', () => {
+ element.updateRanges([rangeB, rangeC]);
+
+ assertHasRange(rangeA, false);
+ assertHasRange(rangeB, true);
+ assertHasRange(rangeC, true);
+
+ element.updateRanges([rangeA, rangeC]);
+
+ assertHasRange(rangeA, true);
+ assertHasRange(rangeB, false);
+ assertHasRange(rangeC, true);
+ });
+
+ test('_getRangesForLine normalizes invalid ranges', () => {
+ const line = new GrDiffLine(GrDiffLineType.BOTH);
+ line.afterNumber = 55;
+ line.text = 'getRangesForLine normalizes invalid ranges';
+ const ranges = element.getRangesForLine(line, Side.RIGHT);
+ assert.equal(ranges.length, 1);
+ const range = ranges[0];
+ assert.isTrue(range.start < range.end, 'start and end are normalized');
+ assert.equal(range.end, line.text.length);
+ });
+ });
+});
diff --git a/polygerrit-ui/app/embed/diff/gr-selection-action-box/gr-selection-action-box.ts b/polygerrit-ui/app/embed/diff/gr-selection-action-box/gr-selection-action-box.ts
index abbb0a3..bb6d1e9 100644
--- a/polygerrit-ui/app/embed/diff/gr-selection-action-box/gr-selection-action-box.ts
+++ b/polygerrit-ui/app/embed/diff/gr-selection-action-box/gr-selection-action-box.ts
@@ -1,26 +1,14 @@
/**
* @license
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
+ * Copyright 2016 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
*/
-import '../../../styles/shared-styles';
import '../../../elements/shared/gr-tooltip/gr-tooltip';
import {GrTooltip} from '../../../elements/shared/gr-tooltip/gr-tooltip';
-import {customElement, property} from '@polymer/decorators';
-import {PolymerElement} from '@polymer/polymer/polymer-element';
-import {htmlTemplate} from './gr-selection-action-box_html';
import {fireEvent} from '../../../utils/event-util';
+import {css, html, LitElement} from 'lit';
+import {customElement, property, query, state} from 'lit/decorators';
+import {sharedStyles} from '../../../styles/shared-styles';
declare global {
interface HTMLElementTagNameMap {
@@ -28,38 +16,64 @@
}
}
-export interface GrSelectionActionBox {
- $: {
- tooltip: GrTooltip;
- };
-}
-
@customElement('gr-selection-action-box')
-export class GrSelectionActionBox extends PolymerElement {
- static get template() {
- return htmlTemplate;
- }
-
+export class GrSelectionActionBox extends LitElement {
/**
* Fired when the comment creation action was taken (click).
*
* @event create-comment-requested
*/
+ @query('#tooltip')
+ tooltip?: GrTooltip;
+
@property({type: Boolean})
positionBelow = false;
+ /**
+ * We need to absolutely position the element before we can show it. So
+ * initially the tooltip must be invisible.
+ */
+ @state() private invisible = true;
+
constructor() {
super();
// See https://crbug.com/gerrit/4767
- this.addEventListener('mousedown', e => this._handleMouseDown(e));
+ this.addEventListener('mousedown', e => this.handleMouseDown(e));
+ }
+
+ static override styles = [
+ sharedStyles,
+ css`
+ :host {
+ cursor: pointer;
+ font-family: var(--font-family);
+ position: absolute;
+ white-space: nowrap;
+ }
+ gr-tooltip[invisible] {
+ visibility: hidden;
+ }
+ `,
+ ];
+
+ override render() {
+ return html`
+ <gr-tooltip
+ id="tooltip"
+ ?invisible=${this.invisible}
+ text="Press c to comment"
+ ?position-below=${this.positionBelow}
+ ></gr-tooltip>
+ `;
}
async placeAbove(el: Text | Element | Range) {
- await this.$.tooltip.updateComplete;
- const rect = this._getTargetBoundingRect(el);
- const boxRect = this.$.tooltip.getBoundingClientRect();
- const parentRect = this._getParentBoundingClientRect();
+ if (!this.tooltip) return;
+ await this.tooltip.updateComplete;
+ const rect = this.getTargetBoundingRect(el);
+ const boxRect = this.tooltip.getBoundingClientRect();
+ const parentRect = this.getParentBoundingClientRect();
if (parentRect === null) {
return;
}
@@ -67,13 +81,15 @@
this.style.left = `${
rect.left - parentRect.left + (rect.width - boxRect.width) / 2
}px`;
+ this.invisible = false;
}
async placeBelow(el: Text | Element | Range) {
- await this.$.tooltip.updateComplete;
- const rect = this._getTargetBoundingRect(el);
- const boxRect = this.$.tooltip.getBoundingClientRect();
- const parentRect = this._getParentBoundingClientRect();
+ if (!this.tooltip) return;
+ await this.tooltip.updateComplete;
+ const rect = this.getTargetBoundingRect(el);
+ const boxRect = this.tooltip.getBoundingClientRect();
+ const parentRect = this.getParentBoundingClientRect();
if (parentRect === null) {
return;
}
@@ -81,9 +97,10 @@
this.style.left = `${
rect.left - parentRect.left + (rect.width - boxRect.width) / 2
}px`;
+ this.invisible = false;
}
- private _getParentBoundingClientRect() {
+ private getParentBoundingClientRect() {
// With native shadow DOM, the parent is the shadow root, not the gr-diff
// element
if (this.parentElement) {
@@ -95,8 +112,8 @@
return null;
}
- // private but used in test
- _getTargetBoundingRect(el: Text | Element | Range) {
+ // visible for testing
+ getTargetBoundingRect(el: Text | Element | Range) {
let rect;
if (el instanceof Text) {
const range = document.createRange();
@@ -109,8 +126,8 @@
return rect;
}
- // private but used in test
- _handleMouseDown(e: MouseEvent) {
+ // visible for testing
+ handleMouseDown(e: MouseEvent) {
if (e.button !== 0) {
return;
} // 0 = main button
diff --git a/polygerrit-ui/app/embed/diff/gr-selection-action-box/gr-selection-action-box_html.ts b/polygerrit-ui/app/embed/diff/gr-selection-action-box/gr-selection-action-box_html.ts
deleted file mode 100644
index 24d63b3..0000000
--- a/polygerrit-ui/app/embed/diff/gr-selection-action-box/gr-selection-action-box_html.ts
+++ /dev/null
@@ -1,33 +0,0 @@
-/**
- * @license
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-import {html} from '@polymer/polymer/lib/utils/html-tag';
-
-export const htmlTemplate = html`
- <style include="shared-styles">
- :host {
- cursor: pointer;
- font-family: var(--font-family);
- position: absolute;
- white-space: nowrap;
- }
- </style>
- <gr-tooltip
- id="tooltip"
- text="Press c to comment"
- position-below="[[positionBelow]]"
- ></gr-tooltip>
-`;
diff --git a/polygerrit-ui/app/embed/diff/gr-selection-action-box/gr-selection-action-box_test.ts b/polygerrit-ui/app/embed/diff/gr-selection-action-box/gr-selection-action-box_test.ts
index 30b7ded..a92c967 100644
--- a/polygerrit-ui/app/embed/diff/gr-selection-action-box/gr-selection-action-box_test.ts
+++ b/polygerrit-ui/app/embed/diff/gr-selection-action-box/gr-selection-action-box_test.ts
@@ -1,51 +1,44 @@
/**
* @license
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
+ * Copyright 2016 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
*/
-
import '../../../test/common-test-setup-karma';
import './gr-selection-action-box';
-import {html} from '@polymer/polymer/lib/utils/html-tag';
import {GrSelectionActionBox} from './gr-selection-action-box';
-import * as MockInteractions from '@polymer/iron-test-helpers/mock-interactions';
import {queryAndAssert} from '../../../test/test-utils';
-
-const basicFixture = fixtureFromTemplate(html`
- <div>
- <gr-selection-action-box></gr-selection-action-box>
- <div class="target">some text</div>
- </div>
-`);
+import {fixture, html} from '@open-wc/testing-helpers';
suite('gr-selection-action-box', () => {
- let container: GrSelectionActionBox;
+ let container: HTMLDivElement;
let element: GrSelectionActionBox;
let dispatchEventStub: sinon.SinonStub;
- setup(() => {
- container = basicFixture.instantiate() as GrSelectionActionBox;
+ setup(async () => {
+ container = await fixture<HTMLDivElement>(html`
+ <div>
+ <gr-selection-action-box></gr-selection-action-box>
+ <div class="target">some text</div>
+ </div>
+ `);
element = queryAndAssert<GrSelectionActionBox>(
container,
'gr-selection-action-box'
);
+ await element.updateComplete;
dispatchEventStub = sinon.stub(element, 'dispatchEvent');
});
+ test('renders', () => {
+ expect(element).shadowDom.to.equal(/* HTML */ `
+ <gr-tooltip invisible id="tooltip" text="Press c to comment"></gr-tooltip>
+ `);
+ });
+
test('ignores regular keys', () => {
- MockInteractions.pressAndReleaseKeyOn(document.body, 27, null, 'esc');
+ const event = new KeyboardEvent('keydown', {key: 'a'});
+ document.body.dispatchEvent(event);
assert.isFalse(dispatchEventStub.called);
});
@@ -61,7 +54,7 @@
});
test('event handled if main button', () => {
- element._handleMouseDown(e);
+ element.handleMouseDown(e);
assert.isTrue(e.preventDefault.called);
assert.equal(
dispatchEventStub.lastCall.args[0].type,
@@ -71,7 +64,7 @@
test('event ignored if not main button', () => {
e.button = 1;
- element._handleMouseDown(e);
+ element.handleMouseDown(e);
assert.isFalse(e.preventDefault.called);
assert.isFalse(dispatchEventStub.called);
});
@@ -92,7 +85,7 @@
height: 6,
} as DOMRect);
getTargetBoundingRectStub = sinon
- .stub(element, '_getTargetBoundingRect')
+ .stub(element, 'getTargetBoundingRect')
.returns({
top: 42,
bottom: 20,
@@ -101,11 +94,20 @@
width: 100,
height: 60,
} as DOMRect);
+ assert.isOk(element.tooltip);
sinon
- .stub(element.$.tooltip, 'getBoundingClientRect')
+ .stub(element.tooltip!, 'getBoundingClientRect')
.returns({width: 10, height: 10} as DOMRect);
});
+ test('renders visible', async () => {
+ await element.placeAbove(target);
+ await element.updateComplete;
+ expect(element).shadowDom.to.equal(/* HTML */ `
+ <gr-tooltip id="tooltip" text="Press c to comment"></gr-tooltip>
+ `);
+ });
+
test('placeAbove for Element argument', async () => {
await element.placeAbove(target);
assert.equal(element.style.top, '25px');
diff --git a/polygerrit-ui/app/embed/diff/gr-syntax-themes/gr-syntax-theme.ts b/polygerrit-ui/app/embed/diff/gr-syntax-themes/gr-syntax-theme.ts
index a41f359..363de13 100644
--- a/polygerrit-ui/app/embed/diff/gr-syntax-themes/gr-syntax-theme.ts
+++ b/polygerrit-ui/app/embed/diff/gr-syntax-themes/gr-syntax-theme.ts
@@ -109,8 +109,6 @@
}
.gr-syntax-strong {
color: var(--syntax-strong-color);
- font-style: var(--syntax-strong-style);
- font-weight: var(--syntax-strong-weight);
}
.gr-syntax-tag {
color: var(--syntax-tag-color);
diff --git a/polygerrit-ui/app/models/bulk-actions/bulk-actions-model.ts b/polygerrit-ui/app/models/bulk-actions/bulk-actions-model.ts
index ad89054..b0984c1 100644
--- a/polygerrit-ui/app/models/bulk-actions/bulk-actions-model.ts
+++ b/polygerrit-ui/app/models/bulk-actions/bulk-actions-model.ts
@@ -129,6 +129,22 @@
});
}
+ voteChanges(reviewInput: ReviewInput) {
+ const current = this.subject$.getValue();
+ return current.selectedChangeNums.map(changeNum => {
+ const change = current.allChanges.get(changeNum)!;
+ if (!change) throw new Error('invalid change id');
+ return this.restApiService.saveChangeReview(
+ change._number,
+ 'current',
+ reviewInput,
+ () => {
+ throw new Error();
+ }
+ );
+ });
+ }
+
addReviewers(
changedReviewers: Map<ReviewerState, AccountInfo[]>
): Promise<Response>[] {
diff --git a/polygerrit-ui/app/models/bulk-actions/bulk-actions-model_test.ts b/polygerrit-ui/app/models/bulk-actions/bulk-actions-model_test.ts
index d4c4a6e..b08455c 100644
--- a/polygerrit-ui/app/models/bulk-actions/bulk-actions-model_test.ts
+++ b/polygerrit-ui/app/models/bulk-actions/bulk-actions-model_test.ts
@@ -7,6 +7,7 @@
import {
createAccountWithIdNameAndEmail,
createChange,
+ createRevisions,
} from '../../test/test-data-generators';
import {
ChangeInfo,
@@ -25,6 +26,7 @@
import {mockPromise} from '../../test/test-utils';
import {SinonStubbedMember} from 'sinon';
import {RestApiService} from '../../services/gr-rest-api/gr-rest-api';
+import {ReviewInput} from '../../types/common';
suite('bulk actions model test', () => {
let bulkActionsModel: BulkActionsModel;
@@ -221,6 +223,61 @@
});
});
+ suite('voteChanges', () => {
+ let detailedActionsStub: SinonStubbedMember<
+ RestApiService['getDetailedChangesWithActions']
+ >;
+ setup(async () => {
+ const c1 = {...createChange(), revisions: createRevisions(10)};
+ c1._number = 1 as NumericChangeId;
+ const c2 = {...createChange(), revisions: createRevisions(4)};
+ c2._number = 2 as NumericChangeId;
+
+ detailedActionsStub = stubRestApi('getDetailedChangesWithActions');
+ detailedActionsStub.returns(
+ Promise.resolve([
+ {...c1, actions: {abandon: {method: HttpMethod.POST}}},
+ {...c2, status: ChangeStatus.ABANDONED},
+ ])
+ );
+
+ await bulkActionsModel.sync([c1, c2]);
+
+ bulkActionsModel.addSelectedChangeNum(c1._number);
+ bulkActionsModel.addSelectedChangeNum(c2._number);
+ });
+
+ test('vote changes', () => {
+ const reviewStub = stubRestApi('saveChangeReview');
+ const reviewInput: ReviewInput = {
+ labels: {
+ a: 1,
+ },
+ };
+ bulkActionsModel.voteChanges(reviewInput);
+ assert.equal(reviewStub.callCount, 2);
+ assert.deepEqual(reviewStub.firstCall.args.slice(0, 3), [
+ 1 as NumericChangeId,
+ 'current',
+ {
+ labels: {
+ a: 1,
+ },
+ },
+ ]);
+
+ assert.deepEqual(reviewStub.secondCall.args.slice(0, 3), [
+ 2 as NumericChangeId,
+ 'current',
+ {
+ labels: {
+ a: 1,
+ },
+ },
+ ]);
+ });
+ });
+
test('stale changes are removed from the model', async () => {
const c1 = createChange();
c1._number = 1 as NumericChangeId;
diff --git a/polygerrit-ui/app/models/comments/comments-model.ts b/polygerrit-ui/app/models/comments/comments-model.ts
index 00f92b5..769d7af 100644
--- a/polygerrit-ui/app/models/comments/comments-model.ts
+++ b/polygerrit-ui/app/models/comments/comments-model.ts
@@ -25,6 +25,7 @@
UrlEncodedCommentId,
PathToCommentsInfoMap,
RobotCommentInfo,
+ PathToRobotCommentsInfoMap,
} from '../../types/common';
import {
addPath,
@@ -50,6 +51,7 @@
import {pluralize} from '../../utils/string-util';
import {ReportingService} from '../../services/gr-reporting/gr-reporting';
import {Model} from '../model';
+import {Deduping} from '../../api/reporting';
export interface CommentState {
/** undefined means 'still loading' */
@@ -376,9 +378,38 @@
const robotComments = await this.restApiService.getDiffRobotComments(
changeNum
);
+ this.reportRobotCommentStats(robotComments);
this.updateState(s => setRobotComments(s, robotComments));
}
+ private reportRobotCommentStats(obj?: PathToRobotCommentsInfoMap) {
+ if (!obj) return;
+ const comments = Object.values(obj).flat();
+ if (comments.length === 0) return;
+ const ids = comments.map(c => c.robot_id);
+ const latestPatchset = comments.reduce(
+ (latestPs, comment) =>
+ Math.max(latestPs, (comment?.patch_set as number) ?? 0),
+ 0
+ );
+ const commentsLatest = comments.filter(c => c.patch_set === latestPatchset);
+ const commentsFixes = comments
+ .map(c => c.fix_suggestions?.length ?? 0)
+ .filter(l => l > 0);
+ const details = {
+ firstId: ids[0],
+ ids: [...new Set(ids)],
+ count: comments.length,
+ countLatest: commentsLatest.length,
+ countFixes: commentsFixes.length,
+ };
+ this.reporting.reportInteraction(
+ Interaction.ROBOT_COMMENTS_STATS,
+ details,
+ {deduping: Deduping.EVENT_ONCE_PER_CHANGE}
+ );
+ }
+
async reloadDrafts(changeNum: NumericChangeId): Promise<void> {
const drafts = await this.restApiService.getDiffDrafts(changeNum);
this.updateState(s => setDrafts(s, drafts));
diff --git a/polygerrit-ui/app/services/gr-reporting/gr-reporting_impl.ts b/polygerrit-ui/app/services/gr-reporting/gr-reporting_impl.ts
index 7404c83..f27ab96 100644
--- a/polygerrit-ui/app/services/gr-reporting/gr-reporting_impl.ts
+++ b/polygerrit-ui/app/services/gr-reporting/gr-reporting_impl.ts
@@ -362,16 +362,18 @@
}
private _reportEvent(eventInfo: EventInfo, opt_noLog?: boolean) {
- const {type, value, name} = eventInfo;
+ const {type, value, name, eventDetails} = eventInfo;
document.dispatchEvent(new CustomEvent(type, {detail: eventInfo}));
if (opt_noLog) {
return;
}
if (type !== ERROR.TYPE) {
if (value !== undefined) {
- console.info(`Reporting: ${name}: ${value}`);
+ console.debug(`Reporting: ${name}: ${value}`);
+ } else if (eventDetails !== undefined) {
+ console.debug(`Reporting: ${name}: ${eventDetails}`);
} else {
- console.info(`Reporting: ${name}`);
+ console.debug(`Reporting: ${name}`);
}
}
}
@@ -614,7 +616,7 @@
LifeCycle.PLUGINS_INSTALLED,
undefined,
{pluginsList: pluginsList || []},
- true
+ false
);
}
@@ -626,7 +628,7 @@
LifeCycle.PLUGINS_FAILED,
undefined,
{pluginsList: pluginsList || []},
- true
+ false
);
}
@@ -755,7 +757,7 @@
eventName,
undefined,
details,
- true
+ false
);
}
@@ -766,7 +768,7 @@
eventName,
undefined,
details,
- true
+ false
);
}
@@ -824,7 +826,7 @@
eventName,
undefined,
details,
- true
+ false
);
}
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 054021b..8e6a147 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
@@ -16,7 +16,6 @@
*/
/* NB: Order is important, because of namespaced classes. */
-import {PolymerElement} from '@polymer/polymer/polymer-element';
import {GrEtagDecorator} from '../../elements/shared/gr-rest-api-interface/gr-etag-decorator';
import {
FetchJSONRequest,
@@ -40,7 +39,6 @@
listChangesOptionsToHex,
} from '../../utils/change-util';
import {assertNever, hasOwnProperty} from '../../utils/common-util';
-import {customElement} from '@polymer/decorators';
import {AuthRequestInit, AuthService} from '../gr-auth/gr-auth';
import {
AccountCapabilityInfo,
@@ -275,12 +273,6 @@
getAppContext().authService.clearCache();
}
-declare global {
- interface HTMLElementTagNameMap {
- 'gr-rest-api-service-impl': GrRestApiServiceImpl;
- }
-}
-
function createReadScheduler() {
return new MaxInFlightScheduler<Response>(new BaseScheduler<Response>(), 10);
}
@@ -288,12 +280,7 @@
function createWriteScheduler() {
return new MaxInFlightScheduler<Response>(new BaseScheduler<Response>(), 5);
}
-
-@customElement('gr-rest-api-service-impl')
-export class GrRestApiServiceImpl
- extends PolymerElement
- implements RestApiService, Finalizable
-{
+export class GrRestApiServiceImpl implements RestApiService, Finalizable {
readonly _cache = siteBasedCache; // Shared across instances.
readonly _sharedFetchPromises = fetchPromisesCache; // Shared across instances.
@@ -311,7 +298,6 @@
private readonly _restApiHelper: GrRestApiHelper;
constructor(authService?: AuthService) {
- super();
// TODO: Make the authService constructor parameter required when we have
// changed all usages of this class to not instantiate via createElement().
this.authService = authService ?? getAppContext().authService;
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 ebbdc9c..d91b438 100644
--- a/polygerrit-ui/app/test/mocks/gr-rest-api_mock.ts
+++ b/polygerrit-ui/app/test/mocks/gr-rest-api_mock.ts
@@ -69,6 +69,7 @@
GroupName,
UrlEncodedRepoName,
NumericChangeId,
+ PreferencesInput,
} from '../../types/common';
import {DiffInfo, DiffPreferencesInfo} from '../../types/diff';
import {readResponsePayload} from '../../elements/shared/gr-rest-api-interface/gr-rest-apis/gr-rest-api-helper';
@@ -84,7 +85,6 @@
import {
createDefaultDiffPrefs,
createDefaultEditPrefs,
- createDefaultPreferences,
} from '../../constants/constants';
import {ParsedChangeInfo} from '../../types/types';
@@ -502,8 +502,9 @@
saveIncludedGroup(): Promise<GroupInfo | undefined> {
throw new Error('saveIncludedGroup() not implemented by RestApiMock.');
},
- savePreferences(): Promise<PreferencesInfo> {
- return Promise.resolve(createDefaultPreferences());
+ savePreferences(input: PreferencesInput): Promise<PreferencesInfo> {
+ const info = input as PreferencesInfo;
+ return Promise.resolve({...info});
},
saveRepoConfig(): Promise<Response> {
return Promise.resolve(new Response());
diff --git a/polygerrit-ui/app/utils/bulk-flow-util.ts b/polygerrit-ui/app/utils/bulk-flow-util.ts
new file mode 100644
index 0000000..9a6179a
--- /dev/null
+++ b/polygerrit-ui/app/utils/bulk-flow-util.ts
@@ -0,0 +1,24 @@
+/**
+ * @license
+ * Copyright 2022 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import {ProgressStatus} from '../constants/constants';
+import {NumericChangeId} from '../api/rest-api';
+
+export function getOverallStatus(
+ progressByChangeNum: Map<NumericChangeId, ProgressStatus>
+) {
+ const statuses = Array.from(progressByChangeNum.values());
+ if (statuses.every(s => s === ProgressStatus.NOT_STARTED)) {
+ return ProgressStatus.NOT_STARTED;
+ }
+ if (statuses.some(s => s === ProgressStatus.RUNNING)) {
+ return ProgressStatus.RUNNING;
+ }
+ if (statuses.some(s => s === ProgressStatus.FAILED)) {
+ return ProgressStatus.FAILED;
+ }
+ return ProgressStatus.SUCCESSFUL;
+}
diff --git a/polygerrit-ui/app/utils/label-util.ts b/polygerrit-ui/app/utils/label-util.ts
index 70b0382..f5703f4 100644
--- a/polygerrit-ui/app/utils/label-util.ts
+++ b/polygerrit-ui/app/utils/label-util.ts
@@ -320,6 +320,15 @@
return `${numberValue}`;
}
+export function getDefaultValue(
+ labels?: LabelNameToInfoMap,
+ labelName?: string
+) {
+ if (!labelName || !labels?.[labelName]) return undefined;
+ const labelInfo = labels[labelName] as DetailedLabelInfo;
+ return labelInfo.default_value;
+}
+
export function getVoteForAccount(
labelName: string,
account?: AccountInfo,
@@ -352,17 +361,30 @@
return Array.from(values.values()).sort((a, b) => a - b);
}
+export function mergeLabelInfoMaps(
+ a?: LabelNameToInfoMap,
+ b?: LabelNameToInfoMap
+): LabelNameToInfoMap {
+ if (!a || !b) return {};
+ const mergedMap: LabelNameToInfoMap = {};
+ for (const key of Object.keys(a)) {
+ if (!hasOwnProperty(b, key)) continue;
+ mergedMap[key] = a[key];
+ }
+ return mergedMap;
+}
+
export function mergeLabelMaps(
a?: LabelNameToValuesMap,
b?: LabelNameToValuesMap
): LabelNameToValuesMap {
if (!a || !b) return {};
- const ans: LabelNameToValuesMap = {};
+ const mergedMap: LabelNameToValuesMap = {};
for (const key of Object.keys(a)) {
if (!hasOwnProperty(b, key)) continue;
- ans[key] = mergeLabelValues(a[key], b[key]);
+ mergedMap[key] = mergeLabelValues(a[key], b[key]);
}
- return ans;
+ return mergedMap;
}
export function mergeLabelValues(a: string[], b: string[]) {
diff --git a/polygerrit-ui/app/utils/label-util_test.ts b/polygerrit-ui/app/utils/label-util_test.ts
index 456ce24..e655789 100644
--- a/polygerrit-ui/app/utils/label-util_test.ts
+++ b/polygerrit-ui/app/utils/label-util_test.ts
@@ -32,6 +32,7 @@
computeLabels,
mergeLabelMaps,
computeOrderedLabelValues,
+ mergeLabelInfoMaps,
} from './label-util';
import {
AccountId,
@@ -302,6 +303,73 @@
]);
});
+ test('mergeLabelInfoMaps', () => {
+ assert.deepEqual(
+ mergeLabelInfoMaps(
+ {
+ A: createDetailedLabelInfo(),
+ B: createDetailedLabelInfo(),
+ },
+ undefined
+ ),
+ {}
+ );
+ assert.deepEqual(
+ mergeLabelInfoMaps(undefined, {
+ A: createDetailedLabelInfo(),
+ B: createDetailedLabelInfo(),
+ }),
+ {}
+ );
+
+ assert.deepEqual(
+ mergeLabelInfoMaps(
+ {
+ A: createDetailedLabelInfo(),
+ B: createDetailedLabelInfo(),
+ },
+ {
+ A: createDetailedLabelInfo(),
+ B: createDetailedLabelInfo(),
+ }
+ ),
+ {
+ A: createDetailedLabelInfo(),
+ B: createDetailedLabelInfo(),
+ }
+ );
+
+ assert.deepEqual(
+ mergeLabelInfoMaps(
+ {
+ A: createDetailedLabelInfo(),
+ B: createDetailedLabelInfo(),
+ },
+ {
+ B: createDetailedLabelInfo(),
+ C: createDetailedLabelInfo(),
+ }
+ ),
+ {
+ B: createDetailedLabelInfo(),
+ }
+ );
+
+ assert.deepEqual(
+ mergeLabelInfoMaps(
+ {
+ A: createDetailedLabelInfo(),
+ B: createDetailedLabelInfo(),
+ },
+ {
+ X: createDetailedLabelInfo(),
+ Y: createDetailedLabelInfo(),
+ }
+ ),
+ {}
+ );
+ });
+
test('mergeLabelMaps', () => {
assert.deepEqual(
mergeLabelMaps(
diff --git a/polygerrit-ui/app/utils/syntax-util.ts b/polygerrit-ui/app/utils/syntax-util.ts
index b9d0597..03774bd 100644
--- a/polygerrit-ui/app/utils/syntax-util.ts
+++ b/polygerrit-ui/app/utils/syntax-util.ts
@@ -38,6 +38,15 @@
.replace(/&/g, '&');
}
+function equal(r: SyntaxLayerRange) {
+ return (s: SyntaxLayerRange) =>
+ r.start === s.start && r.length === s.length && r.className === s.className;
+}
+
+function unique(r: SyntaxLayerRange, index: number, array: SyntaxLayerRange[]) {
+ return index === array.findIndex(equal(r));
+}
+
/**
* HighlightJS produces one long HTML string with HTML elements spanning
* multiple lines. <gr-diff> is line based, needs all elements closed at the end
@@ -87,7 +96,9 @@
range.length = lineLength - range.start;
}
}
- rangesPerLine.push({ranges: ranges.filter(r => r.length > 0)});
+ rangesPerLine.push({
+ ranges: ranges.filter(r => r.length > 0).filter(unique),
+ });
}
if (carryOverRanges.length > 0) {
throw new Error('unclosed <span>s in highlighted code');
diff --git a/polygerrit-ui/app/utils/syntax-util_test.ts b/polygerrit-ui/app/utils/syntax-util_test.ts
index 81cdf57..4d381fb 100644
--- a/polygerrit-ui/app/utils/syntax-util_test.ts
+++ b/polygerrit-ui/app/utils/syntax-util_test.ts
@@ -80,6 +80,15 @@
);
});
+ test('removal of duplicate spans', async () => {
+ assert.deepEqual(
+ highlightedStringToRanges(
+ '<span class="d"><span class="d">asdfqwer</span></span>'
+ ),
+ [{ranges: [{start: 0, length: 8, className: 'd'}]}]
+ );
+ });
+
test('one line, two spans one after another', async () => {
assert.deepEqual(
highlightedStringToRanges(